Manajemen Ketergantungan Python: Perbandingan Pendekatan

gambar

Saya telah menulis dengan python selama lima tahun, dimana tiga tahun terakhir telah mengembangkan proyek saya sendiri. Sebagian besar cara tim saya membantu saya dengan ini. Dan dengan setiap rilis, dengan setiap fitur baru, kami semakin berusaha untuk memastikan bahwa proyek tidak berubah menjadi berantakan dari kode yang tidak didukung; kami berjuang dengan impor siklik, saling ketergantungan, mengalokasikan modul yang dapat digunakan kembali, membangun kembali struktur.

Sayangnya, dalam komunitas Python tidak ada konsep universal "arsitektur yang baik", hanya ada konsep "pythonicity", jadi kita harus membuat sendiri arsitekturnya. Di bawah cut - Longrid dengan refleksi pada arsitektur, dan pertama - tama - pada manajemen dependensi berlaku untuk Python.

django.setup ()


Saya akan mulai dengan pertanyaan kepada para dzhangis. Apakah Anda sering menulis dua baris ini?

import django django.setup() 

Anda perlu memulai file dari ini jika Anda ingin bekerja dengan objek Django tanpa memulai server web Django itu sendiri. Ini berlaku untuk model, dan alat untuk bekerja dengan waktu ( django.utils.timezone ), dan django.urls.reverse ( django.urls.reverse ), dan banyak lagi. Jika ini tidak dilakukan, maka Anda akan mendapatkan kesalahan:

 django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet. 

Saya terus menulis dua baris ini. Saya penggemar berat kode ejeksi; Saya suka membuat file .py terpisah, memutar hal-hal di dalamnya, mencari tahu - dan kemudian menanamkannya dalam proyek.

Dan django.setup() konstan ini django.setup() mengganggu saya. Pertama, Anda bosan mengulanginya di mana-mana; dan, kedua, inisialisasi Django membutuhkan waktu beberapa detik (kami memiliki monolith besar), dan ketika Anda me-restart file yang sama 10, 20, 100 kali - itu hanya memperlambat pengembangan.

Bagaimana cara menyingkirkan django.setup() ? Anda perlu menulis kode yang minimal tergantung pada Django.

Sebagai contoh, jika kita menulis klien dari API eksternal, maka kita dapat membuatnya bergantung pada Django:

 from django.conf import settings class APIClient: def __init__(self): self.api_key = settings.SOME_API_KEY # : client = APIClient() 

atau itu bisa independen dari Django:

 class APIClient: def __init__(self, api_key): self.api_key = api_key # : client = APIClient(api_key='abc') 

Dalam kasus kedua, konstruktor lebih rumit, tetapi setiap manipulasi dengan kelas ini dapat dilakukan tanpa memuat seluruh mesin dzhangovskoy.

Tes juga semakin mudah. Bagaimana cara menguji komponen yang tergantung pada pengaturan django.conf.settings ? Cukup kunci mereka dengan dekorator @override_settings . Dan jika komponen tidak bergantung pada apa pun, maka tidak akan ada yang menjadi basah: ia melewati parameter ke konstruktor - dan mengendarainya.

Manajemen ketergantungan


Kisah dependensi django adalah contoh paling mencolok dari masalah yang saya temui setiap hari: masalah manajemen dependensi dalam python - dan arsitektur keseluruhan aplikasi python.

Hubungan dengan manajemen ketergantungan dalam komunitas Python beragam. Tiga kamp utama dapat dibedakan:

  • Python adalah bahasa yang fleksibel. Kami menulis seperti yang kami inginkan, tergantung pada apa yang kami inginkan. Kami tidak malu tentang dependensi siklik, substitusi atribut untuk kelas dalam runtime, dll.

  • Python adalah bahasa khusus. Ada cara-cara idiomatis untuk membangun arsitektur dan dependensi. Transfer data naik dan turun dari tumpukan panggilan dilakukan oleh iterator, coroutine, dan manajer konteks.

    Laporan kelas tentang subjek dan contoh ini
    Brandon Rhodes, Dropbox: Hoist IO Anda .

    Contoh dari laporan:

     def main(): """          """ with open("/etc/hosts") as file: for line in parse_hosts(file): print(line) def parse_hosts(lines): """    -   """ for line in lines: if line.startswith("#"): continue yield line 


  • Fleksibilitas Python adalah cara ekstra untuk menembak diri sendiri di kaki. Anda memerlukan seperangkat aturan kaku untuk mengelola dependensi. Contoh yang baik adalah orang - orang python kering Rusia. Masih ada pendekatan yang kurang hardcore - struktur Django untuk skala dan umur panjang , Tapi idenya sama.

Ada beberapa artikel tentang manajemen dependensi dalam python ( contoh 1 , contoh 2 ), tetapi mereka semua turun untuk mengiklankan kerangka Ketergantungan Injeksi seseorang. Artikel ini adalah entri baru pada topik yang sama, tetapi kali ini merupakan eksperimen pemikiran murni tanpa iklan. Ini adalah upaya untuk menemukan keseimbangan antara tiga pendekatan di atas, lakukan tanpa kerangka kerja tambahan dan menjadikannya "pythonic".

Saya baru-baru ini membaca Arsitektur Bersih - dan saya tampaknya mengerti apa nilai ketergantungan injeksi dalam python dan bagaimana itu dapat diimplementasikan. Saya melihat ini pada contoh proyek saya sendiri. Singkatnya, ini melindungi kode dari kerusakan ketika kode lain berubah .

Sumber data


Ada klien API yang mengeksekusi permintaan HTTP untuk penyingkat layanan:

 # shortener_client.py import requests class ShortenerClient: def __init__(self, api_key): self.api_key = api_key def shorten_link(self, url): response = requests.post( url='https://fstrk.cc/short', headers={'Authorization': self.api_key}, json={'url': url} ) return response.json()['url'] 

Dan ada modul yang mempersingkat semua tautan dalam teks. Untuk melakukan ini, ia menggunakan klien API shortener:

 # text_processor.py import re from shortener_client import ShortenerClient class TextProcessor: def __init__(self, text): self.text = text def process(self): changed_text = self.text links = re.findall( r'https?://[^\r\n\t") ]*', self.text, flags=re.MULTILINE ) api_client = ShortenerClient('abc') for link in links: shortened = api_client.shorten_link(link) changed_text = changed_text.replace(link, shortened) return changed_text 

Logika eksekusi kode hidup dalam file kontrol yang terpisah (sebut saja itu pengontrol):

 # controller.py from text_processor import TextProcessor processor = TextProcessor("""  1: https://ya.ru  2: https://google.com """) print(processor.process()) 

Semuanya berfungsi. Prosesor mem-parsing teks, memperpendek tautan menggunakan penyingkat, mengembalikan hasilnya. Ketergantungan terlihat seperti ini:

gambar

Masalah


Inilah masalahnya: kelas TextProcessor tergantung pada kelas ShortenerClient - dan rusak ketika antarmuka ShortenerClient berubah .

Bagaimana ini bisa terjadi?

Misalkan dalam proyek kami, kami memutuskan untuk melacak shorten_link dan menambahkan argumen callback_url ke metode shorten_link . Argumen ini berarti alamat yang pemberitahuannya harus datang saat mengklik tautan.

Metode ShortenerClient.shorten_link mulai terlihat seperti ini:

 def shorten_link(self, url, callback_url): response = requests.post( url='https://fstrk.cc/short', headers={'Authorization': self.api_key}, json={'url': url, 'callback_on_click': callback_url} ) return response.json()['url'] 

Dan apa yang terjadi? Dan ternyata ketika kami mencoba memulai, kami mendapat kesalahan:

 TypeError: shorten_link() missing 1 required positional argument: 'callback_url' 

Yaitu, kami mengganti shortener, tetapi bukan dia yang bangkrut, tetapi kliennya:

gambar

Jadi apa Nah, file panggilan terputus, kami pergi dan memperbaikinya. Apa masalahnya?

Jika ini diselesaikan dalam satu menit - mereka pergi dan diperbaiki - maka ini, tentu saja, tidak masalah sama sekali. Jika ada sedikit kode di kelas dan jika Anda mendukungnya sendiri (ini adalah proyek sampingan Anda, ini adalah dua kelas kecil dari subsistem yang sama, dll.), Maka Anda bisa berhenti di sana.

Masalah mulai ketika:

  • modul pemanggil dan dipanggil memiliki banyak kode;
  • modul yang berbeda didukung oleh orang / tim yang berbeda.

Jika Anda menulis kelas ShortenerClient , dan kolega Anda menulis TextProcessor , Anda mendapatkan situasi ofensif: Anda mengubah kode, tetapi itu rusak. Dan itu pecah di tempat yang belum pernah Anda lihat dalam hidup, dan sekarang Anda perlu duduk dan memahami kode orang lain.

Yang lebih menarik adalah ketika modul Anda digunakan di beberapa tempat, dan bukan di satu; dan hasil edit Anda akan memecah kode pada tumpukan file.

Oleh karena itu, masalahnya dapat dirumuskan sebagai berikut: bagaimana mengatur kode sehingga ketika antarmuka ShortenerClient diubah, ShortenerClient sendiri ShortenerClient , dan bukan penggunanya (yang jumlahnya bisa banyak)?

Solusinya di sini adalah:

  • Konsumen kelas dan kelas itu sendiri harus menyetujui antarmuka umum. Antarmuka ini harus menjadi hukum.
  • Jika kelas berhenti sesuai dengan antarmuka, ini akan menjadi masalahnya, dan bukan masalah konsumen.

gambar

Bekukan antarmuka


Seperti apa cara memperbaiki tampilan antarmuka dengan python? Ini adalah kelas abstrak:

 from abc import ABC, abstractmethod class AbstractClient(ABC): @abstractmethod def __init__(self, api_key): pass @abstractmethod def shorten_link(self, link): pass 

Jika sekarang kita mewarisi dari kelas ini dan lupa mengimplementasikan beberapa metode, kita akan mendapatkan kesalahan:

 class ShortenerClient(AbstractClient): def __ini__(self, api_key): self.api_key = api_key client = ShortenerClient('123') >>> TypeError: Can't instantiate abstract class ShortenerClient with abstract methods __init__, shorten_link 

Tetapi ini tidak cukup. Kelas abstrak hanya menangkap nama-nama metode, tetapi tidak tanda tangan mereka.

Butuh alat verifikasi tanda tangan kedua Alat kedua ini mypy . Ini akan membantu memverifikasi tanda tangan dari metode yang diwariskan. Untuk melakukan ini, kita harus menambahkan anotasi ke antarmuka:

 # shortener_client.py from abc import ABC, abstractmethod class AbstractClient(ABC): @abstractmethod def __init__(self, api_key: str) -> None: pass @abstractmethod def shorten_link(self, link: str) -> str: pass class ShortenerClient(AbstractClient): def __init__(self, api_key: str) -> None: self.api_key = api_key def shorten_link(self, link: str, callback_url: str) -> str: return 'xxx' 

Jika sekarang kami memeriksa kode ini dengan mypy , kami mendapatkan kesalahan karena argumen callback_url tambahan:

 mypy shortener_client.py >>> error: Signature of "shorten_link" incompatible with supertype "AbstractClient" 

Sekarang kita memiliki cara yang dapat diandalkan untuk melakukan antarmuka kelas.

Ketergantungan inversi


Setelah mendebug antarmuka, kita harus memindahkannya ke tempat lain untuk sepenuhnya menghilangkan ketergantungan konsumen pada file shortener_client.py . Misalnya, Anda dapat menyeret antarmuka langsung ke konsumen - ke file dengan prosesor TextProcessor :

 # text_processor.py import re from abc import ABC, abstractmethod class AbstractClient(ABC): @abstractmethod def __init__(self, api_key: str) -> None: pass @abstractmethod def shorten_link(self, link: str) -> str: pass class TextProcessor: def __init__(self, text, shortener_client: AbstractClient) -> None: self.text = text self.shortener_client = shortener_client def process(self) -> str: changed_text = self.text links = re.findall( r'https?://[^\r\n\t") ]*', self.text, flags=re.MULTILINE ) for link in links: shortened = self.shortener_client.shorten_link(link) changed_text = changed_text.replace(link, shortened) return changed_text 

Dan itu akan mengubah arah kecanduan! Sekarang TextProcessor memiliki antarmuka interaksi, dan sebagai hasilnya, ShortenerClient bergantung padanya, dan bukan sebaliknya.

gambar

Dengan kata-kata sederhana, kita dapat menggambarkan esensi dari transformasi kita sebagai berikut:

  • TextProcessor mengatakan: Saya seorang prosesor, dan saya terlibat dalam konversi teks. Saya tidak ingin tahu apa-apa tentang mekanisme pemendekan: ini bukan urusan saya. Saya ingin menarik metode shorten_link sehingga shorten_link segalanya untuk saya. Jadi tolong, beri saya objek yang bermain sesuai dengan aturan saya. Keputusan tentang bagaimana saya berinteraksi dibuat oleh saya, bukan dia.
  • ShortenerClient mengatakan: sepertinya saya tidak bisa hidup dalam ruang hampa, dan mereka membutuhkan perilaku tertentu dari saya. Saya akan bertanya pada TextProcessor apa yang harus saya cocokkan agar tidak rusak.

Banyak konsumen


Jika beberapa modul menggunakan tautan pemendekan, maka antarmuka harus diletakkan bukan di salah satu dari mereka, tetapi di beberapa file terpisah, yang terletak di atas file lain, lebih tinggi dalam hierarki:

gambar

Komponen kontrol


Jika konsumen tidak mengimpor ShortenerClient , lalu siapa yang akan mengimpor dan membuat objek kelas? Itu harus menjadi komponen kontrol - dalam kasus kami itu adalah controller.py .

Pendekatan paling sederhana adalah injeksi ketergantungan langsung, Injeksi Ketergantungan "di dahi". Kami membuat objek dalam kode panggilan, mentransfer satu objek ke objek lainnya. Untung

 # controller.py import TextProcessor import ShortenerClient processor = TextProcessor( text=' 1: https://ya.ru  2: https://google.com', shortener_client=ShortenerClient(api_key='123') ) print(processor.process()) 

Pendekatan python


Pendekatan yang lebih "pythonic" diyakini sebagai Injeksi Ketergantungan melalui warisan.

Raymond Hettinger membicarakan hal ini dengan sangat terperinci dalam laporan Super yang dianggapnya Super.

Untuk menyesuaikan kode dengan gaya ini, Anda perlu sedikit mengubah TextProcessor , membuatnya dapat diwarisi:

 # text_processor.py class TextProcessor: def __init__(self, text: str) -> None: self.text = text self.shortener_client: AbstractClient = self.get_shortener_client() def get_shortener_client(self) -> AbstractClient: """      """ raise NotImplementedError 

Dan kemudian, dalam kode panggilan, mewarisinya:

 # controller.py import TextProcessor import ShortenerClient class ProcessorWithClient(TextProcessor): """   ,    """ def get_shortener_client(self) -> ShortenerClient: return ShortenerClient(api_key='abc') processor = ProcessorWithClient( text=' 1: https://ya.ru  2: https://google.com' ) print(processor.process()) 

Contoh kedua ada di mana-mana dalam kerangka kerja populer:

  • Di Django, kami terus diwariskan. Kami mendefinisikan kembali metode tampilan, model, formulir berbasis kelas; dengan kata lain, suntikkan dependensi kami ke dalam kerangka kerja yang sudah didebitkan.
  • Di DRF, hal yang sama. Kami memperluas pandangan, serialisator, izin.
  • Dan sebagainya. Ada banyak contoh.

Contoh kedua terlihat lebih cantik dan lebih akrab, bukan? Mari kita kembangkan dan lihat apakah kecantikan ini terjaga.

Pengembangan python


Dalam logika bisnis, biasanya ada lebih dari dua komponen. Misalkan TextProcessor kami bukan kelas independen, tetapi hanya salah satu elemen TextPipeline yang memproses teks dan mengirimkannya ke surat:

 class TextPipeline: def __init__(self, text, email): self.text_processor = TextProcessor(text) self.mailer = Mailer(email) def process_and_mail(self) -> None: processed_text = self.text_processor.process() self.mailer.send_text(text=processed_text) 

Jika kita ingin mengisolasi TextPipeline dari kelas yang digunakan, kita harus mengikuti prosedur yang sama seperti sebelumnya:

  • kelas TextPipeline akan mendeklarasikan antarmuka untuk komponen yang digunakan;
  • komponen yang digunakan akan dipaksa untuk menyesuaikan diri dengan antarmuka ini;
  • beberapa kode eksternal akan menyatukan dan menjalankan semuanya.

Diagram dependensi akan terlihat seperti ini:

gambar

Tapi seperti apa kode assembly dari dependensi ini sekarang?

 import TextProcessor import ShortenerClient import Mailer import TextPipeline class ProcessorWithClient(TextProcessor): def get_shortener_client(self) -> ShortenerClient: return ShortenerClient(api_key='123') class PipelineWithDependencies(TextPipeline): def get_text_processor(self, text: str) -> ProcessorWithClient: return ProcessorWithClient(text) def get_mailer(self, email: str) -> Mailer: return Mailer(email) pipeline = PipelineWithDependencies( email='abc@def.com', text=' 1: https://ya.ru  2: https://google.com' ) pipeline.process_and_mail() 

Pernahkah Anda memperhatikan? Kami pertama kali mewarisi kelas TextProcessor untuk menyisipkan ShortenerClient ke dalamnya, dan kemudian mewarisi TextPipeline untuk memasukkan TextProcessor kami yang telah didefinisikan ulang (serta Mailer ) ke dalamnya. Kami memiliki beberapa tingkat redefinisi berurutan. Sudah rumit.

Mengapa semua kerangka kerja diatur dengan cara ini? Ya, karena hanya cocok untuk kerangka kerja.

  • Semua tingkatan kerangka kerja didefinisikan dengan jelas, dan jumlahnya terbatas. Misalnya, di Django, Anda bisa mengesampingkan FormField untuk menyisipkannya ke override Form , untuk menyisipkan formulir ke override tampilan. Itu saja. Tiga level.
  • Setiap kerangka kerja melayani satu tujuan. Tugas ini didefinisikan dengan jelas.
  • Setiap kerangka kerja memiliki dokumentasi terperinci yang menjelaskan bagaimana dan apa yang akan diwarisi; apa dan dengan apa yang harus digabungkan.

Dapatkah Anda dengan jelas dan jelas mengidentifikasi dan mendokumentasikan logika bisnis Anda? Terutama arsitektur tingkat di mana ia bekerja? Saya tidak. Sayangnya, pendekatan Raymond Hettinger tidak mempengaruhi logika bisnis.

Kembali ke pendekatan dahi


Pada beberapa tingkat kesulitan, pendekatan sederhana menang. Itu terlihat lebih sederhana - dan lebih mudah untuk berubah ketika logika berubah.

 import TextProcessor import ShortenerClient import Mailer import TextPipeline pipeline = TextPipeline( text_processor=TextProcessor( text=' 1: https://ya.ru  2: https://google.com', shortener_client=ShortenerClient(api_key='abc') ), mailer=Mailer('abc@def.com') ) pipeline.process_and_mail() 

Tetapi, ketika jumlah level logika meningkat, bahkan pendekatan ini menjadi tidak nyaman. Kita harus secara agresif memulai banyak kelas, melewati mereka satu sama lain. Saya ingin menghindari banyak level sarang.

Mari kita coba satu panggilan lagi.

Penyimpanan Instance Global


Mari kita coba membuat kamus global di mana instance dari komponen yang kita butuhkan berada. Dan biarkan komponen-komponen ini saling terhubung melalui akses ke kamus ini.

Sebut saja INSTANCE_DICT :

 # text_processor.py import INSTANCE_DICT class TextProcessor(AbstractTextProcessor): def __init__(self, text) -> None: self.text = text def process(self) -> str: shortener_client: AbstractClient = INSTANCE_DICT['Shortener'] # ...   

 # text_pipeline.py import INSTANCE_DICT class TextPipeline: def __init__(self) -> None: self.text_processor: AbstractTextProcessor = INSTANCE_DICT[ 'TextProcessor'] self.mailer: AbstractMailer = INSTANCE_DICT['Mailer'] def process_and_mail(self) -> None: processed_text = self.text_processor.process() self.mailer.send_text(text=processed_text) 

Caranya adalah dengan meletakkan objek kami di kamus ini sebelum diakses . Inilah yang akan kita lakukan di controller.py :

 # controller.py import INSTANCE_DICT import TextProcessor import ShortenerClient import Mailer import TextPipeline INSTANCE_DICT['Shortener'] = ShortenerClient('123') INSTANCE_DICT['Mailer'] = Mailer('abc@def.com') INSTANCE_DICT['TextProcessor'] = TextProcessor(text=' : https://ya.ru') pipeline = TextPipeline() pipeline.process_and_mail() 

Keuntungan bekerja melalui kamus global:

  • tidak ada keajaiban kap mesin dan kerangka DI tambahan;
  • daftar datar dependensi di mana Anda tidak perlu mengelola bersarang;
  • semua bonus DI: pengujian sederhana, independensi, perlindungan komponen dari kerusakan saat komponen lain berubah.

Tentu saja, alih-alih membuat INSTANCE_DICT , Anda dapat menggunakan semacam kerangka DI; tetapi esensi dari ini tidak akan berubah. Kerangka kerja ini akan memberikan manajemen instance yang lebih fleksibel; dia akan memungkinkan Anda untuk membuatnya dalam bentuk singletone atau bundel, seperti pabrik; tetapi idenya akan tetap sama.

Mungkin pada titik tertentu ini tidak akan cukup bagi saya, dan saya masih memilih semacam kerangka kerja.

Dan, mungkin, semua ini tidak perlu, dan lebih mudah untuk melakukannya tanpa itu: tulis impor langsung dan tidak membuat antarmuka abstrak yang tidak perlu.

Apa pengalaman Anda dengan manajemen ketergantungan di python? Dan secara umum - apakah perlu, atau saya menemukan masalah dari udara?

Source: https://habr.com/ru/post/id461511/


All Articles