
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
atau itu bisa independen dari Django:
class APIClient: def __init__(self, api_key): self.api_key = api_key
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 iniBrandon 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:
Dan ada modul yang mempersingkat semua tautan dalam teks. Untuk melakukan ini, ia menggunakan klien API shortener:
Logika eksekusi kode hidup dalam file kontrol yang terpisah (sebut saja itu pengontrol):
Semuanya berfungsi. Prosesor mem-parsing teks, memperpendek tautan menggunakan penyingkat, mengembalikan hasilnya. Ketergantungan terlihat seperti ini:

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:

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.

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:
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
:
Dan itu akan mengubah arah kecanduan! Sekarang
TextProcessor
memiliki antarmuka interaksi, dan sebagai hasilnya,
ShortenerClient
bergantung padanya, dan bukan sebaliknya.

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:

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
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:
Dan kemudian, dalam kode panggilan, mewarisinya:
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:

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
:
Caranya adalah dengan
meletakkan objek kami di kamus ini sebelum diakses . Inilah yang akan kita lakukan di
controller.py
:
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?