Arsitektur Bersih melalui mata pengembang Python

Halo! Nama saya Eugene, saya adalah pengembang Python. Selama satu setengah tahun terakhir, tim kami mulai secara aktif menerapkan prinsip Arsitektur Bersih, menjauh dari model MVC klasik. Dan hari ini saya akan berbicara tentang bagaimana kita sampai pada ini, apa yang diberikannya kepada kita, dan mengapa transfer langsung pendekatan dari PL lain tidak selalu merupakan solusi yang baik.



Python telah menjadi alat pengembangan utama saya selama lebih dari tujuh tahun sekarang. Ketika mereka bertanya kepada saya apa yang paling saya sukai darinya, saya menjawab bahwa ini adalah keterbacaannya yang luar biasa . Kenalan pertama dimulai dengan membaca buku "Programming a Collective Mind" . Saya tertarik pada algoritma yang dijelaskan di dalamnya, tetapi semua contohnya dalam bahasa yang belum saya kenal saat itu. Ini tidak biasa (Python belum menjadi arus utama dalam pembelajaran mesin), daftar sering ditulis dalam pseudo-code atau menggunakan diagram. Tapi setelah pengantar singkat ke bahasa, saya menghargai keringkasannya: semuanya mudah dan jelas, tidak ada yang berlebihan dan mengganggu, hanya inti dari proses yang dijelaskan. Kelebihan utama dari ini adalah desain bahasa yang menakjubkan, gula sintaksis yang sangat intuitif. Ekspresi ini selalu dihargai di masyarakat. Apa itu " impor ini ", harus ada di halaman pertama buku teks apa pun: sepertinya pengawas yang tak terlihat, terus-menerus mengevaluasi tindakan Anda. Di forum, layak pemula untuk menggunakan entah bagaimana CamelCase dalam nama variabel dalam daftar, jadi segera sudut diskusi bergeser ke arah idiom kode yang diusulkan dengan referensi ke PEP8.
Mengejar keanggunan plus dinamisme bahasa yang kuat telah menciptakan banyak perpustakaan dengan API yang benar-benar menyenangkan. 

Meskipun demikian, Python, walaupun kuat, hanyalah alat yang memungkinkan Anda untuk menulis kode yang mendokumentasikan diri sendiri, tetapi tidak menjamin hal ini , juga kepatuhan PEP8. Ketika toko online kami yang tampaknya sederhana di Django mulai menghasilkan uang dan, sebagai hasilnya, memompa fitur, pada satu titik kami menyadari bahwa itu tidak begitu sederhana, dan bahkan membuat perubahan mendasar memerlukan lebih banyak usaha, dan yang paling penting, tren ini berkembang. Apa yang terjadi dan ketika semuanya salah?

Kode salah


Kode buruk bukan salah satu yang tidak mengikuti PEP8 atau tidak memenuhi persyaratan kompleksitas siklomatik. Pertama-tama, kode salah adalah dependensi yang tidak terkontrol , yang mengarah pada fakta bahwa perubahan di satu tempat program menyebabkan perubahan yang tidak terduga di bagian lain. Kami kehilangan kendali atas kode, memperluas fungsionalitas memerlukan studi rinci dari proyek. Kode tersebut kehilangan fleksibilitasnya dan, seolah-olah, menolak melakukan perubahan, sementara program itu sendiri menjadi "rapuh". 

Arsitektur bersih


Arsitektur aplikasi yang dipilih harus menghindari masalah ini, dan kami bukan orang pertama yang menemukan ini: telah ada diskusi di komunitas Java tentang membuat desain aplikasi yang optimal untuk waktu yang lama.

Kembali pada tahun 2000, Robert Martin (juga dikenal sebagai Paman Bob) dalam artikelnya " Prinsip Desain dan Desain " menyatukan lima prinsip untuk merancang aplikasi OOP di bawah akronim SOLID. Prinsip-prinsip ini telah diterima dengan baik oleh masyarakat dan telah jauh melampaui ekosistem Jawa. Namun demikian, mereka sangat abstrak. Kemudian ada beberapa upaya untuk mengembangkan desain aplikasi umum berdasarkan prinsip SOLID. Ini termasuk: "Arsitektur heksagonal", "Port dan adaptor", "Arsitektur Bulbous" dan mereka semua memiliki banyak kesamaan, meskipun detail implementasi yang berbeda. Dan pada 2012, sebuah artikel diterbitkan oleh Robert Martin yang sama, di mana ia mengusulkan versinya sendiri yang disebut " Arsitektur Bersih ".



Menurut Paman Bob, arsitektur terutama adalah " batas dan hambatan ", perlu untuk memahami dengan jelas kebutuhan dan membatasi antarmuka perangkat lunak agar tidak kehilangan kendali atas aplikasi. Untuk melakukan ini, program ini dibagi menjadi beberapa lapisan. Beralih dari satu lapisan ke lapisan lain, hanya data yang dapat ditransfer (struktur sederhana dan objek DTO dapat bertindak sebagai data ) - ini adalah aturan batas. Ungkapan lain yang paling sering dikutip bahwa " aplikasi harus berteriak " berarti bahwa hal utama dalam aplikasi bukanlah kerangka kerja yang digunakan atau teknologi penyimpanan data, tetapi apa yang sebenarnya dilakukan aplikasi ini, fungsi apa yang dijalankannya - logika bisnis dari aplikasi tersebut . Oleh karena itu, lapisan tidak memiliki struktur linier, tetapi memiliki hierarki . Karenanya dua aturan lagi:

  • Aturan prioritas untuk lapisan dalam - itu adalah lapisan dalam yang menentukan antarmuka di mana ia akan berinteraksi dengan dunia luar;
  • Ketergantungan aturan - Ketergantungan harus diarahkan dari lapisan dalam ke luar.

Aturan terakhir cukup tidak lazim di dunia Python. Untuk menerapkan skenario logika bisnis yang rumit, Anda selalu perlu mengakses layanan eksternal (misalnya, basis data), tetapi untuk menghindari ketergantungan ini, lapisan logika bisnis itu sendiri harus menyatakan antarmuka yang dengannya ia akan berinteraksi dengan dunia luar. Teknik ini disebut " dependensi inversi " (huruf D dalam SOLID) dan banyak digunakan dalam bahasa dengan pengetikan statis. Menurut Robert Martin, ini adalah keunggulan utama yang datang dengan OOP .

Tiga aturan ini adalah inti dari Arsitektur Bersih:

  • Aturan lintas batas;
  • Aturan ketergantungan;
  • Aturan prioritas lapisan dalam.

Keuntungan dari pendekatan ini meliputi:

  • Kemudahan pengujian - lapisan terisolasi, masing-masing, mereka dapat diuji tanpa monyet-patching, Anda dapat mengatur pelapisan untuk lapisan yang berbeda, tergantung pada tingkat kepentingannya;
  • Kemudahan mengubah aturan bisnis , karena semuanya dikumpulkan di satu tempat, tidak tersebar di proyek dan tidak dicampur dengan kode tingkat rendah;
  • Independensi dari agen eksternal : kehadiran abstraksi antara logika bisnis dan dunia luar dalam kasus tertentu memungkinkan Anda untuk mengubah sumber eksternal tanpa mempengaruhi lapisan internal. Ini berfungsi jika Anda belum mengikat logika bisnis dengan fitur spesifik agen eksternal, misalnya, transaksi basis data;
  • , , , .

, . . , . Β« Clean ArchitectureΒ».

Python


Ini adalah teori, contoh penerapan praktis dapat ditemukan dalam artikel asli, laporan dan buku Robert Martin. Mereka mengandalkan beberapa pola desain umum dari dunia Java: Adaptor, Gateway, Interactor, Fasade, Repository, DTO , dll.

Nah, bagaimana dengan Python? Seperti yang saya katakan, laconicism dihargai di komunitas Python. Apa yang telah berakar pada orang lain jauh dari kenyataan bahwa ia akan berakar bersama kami. Pertama kali saya beralih ke topik ini tiga tahun lalu, maka tidak ada banyak materi tentang topik menggunakan Arsitektur Bersih dengan Python, tetapi tautan pertama di Google adalah proyek Leonardo Giordani: penulis menjelaskan secara terperinci proses pembuatan API untuk situs pencarian properti menggunakan metode TDD, menerapkan Arsitektur Bersih.
Sayangnya, terlepas dari penjelasan yang cermat dan mengikuti semua kanon Paman Bob, contoh ini agak menakutkan

API proyek terdiri dari satu metode - mendapatkan daftar dengan filter yang tersedia. Saya berpikir bahwa bahkan untuk pengembang pemula, kode untuk proyek semacam itu akan mengambil tidak lebih dari 15 baris. Tetapi dalam kasus ini, ia mengambil enam paket. Anda dapat merujuk ke tata letak yang tidak sepenuhnya berhasil, dan ini benar, tetapi dalam hal apa pun, sulit bagi seseorang untuk menjelaskan efektivitas pendekatan ini , merujuk pada proyek ini.

Ada masalah yang lebih serius, jika Anda tidak membaca artikel dan segera mulai mempelajari proyek, maka itu cukup sulit untuk dipahami. Pertimbangkan penerapan logika bisnis:

from rentomatic.response_objects import response_objects as res

class RoomListUseCase(object):
   def __init__(self, repo):
       self.repo = repo
   def execute(self, request_object):
       if not request_object:
           return res.ResponseFailure.build_from_invalid_request_object(
               request_object)
       try:
           rooms = self.repo.list(filters=request_object.filters)
           return res.ResponseSuccess(rooms)
       except Exception as exc:
           return res.ResponseFailure.build_system_error(
               "{}: {}".format(exc.__class__.__name__, "{}".format(exc)))

Kelas RoomListUseCase yang mengimplementasikan logika bisnis (tidak terlalu mirip dengan logika bisnis, kan?) Dari proyek diinisialisasi oleh objek repo . Tapi apa itu repo ? Tentu saja, dari konteksnya, kita dapat memahami bahwa repo mengimplementasikan templat Repositori untuk mengakses data, jika kita melihat isi RoomListUseCase, kita memahami bahwa itu harus memiliki satu metode daftar, input yang merupakan daftar filter, yang tidak jelas pada output, Anda perlu melihat di ResponseSuccess. Dan jika skenarionya lebih kompleks, dengan beberapa akses ke sumber data? Ternyata untuk memahami apa itu repo, Anda hanya bisa merujuk pada implementasinya. Tapi di mana dia berada? Itu terletak pada modul terpisah, yang sama sekali tidak terkait dengan RoomListUseCase. Jadi, untuk memahami apa yang terjadi, Anda perlu naik ke level atas (level framework) dan melihat apa yang diumpankan ke input kelas ketika membuat objek.

Anda mungkin berpikir bahwa saya mencantumkan kerugian dari pengetikan dinamis, tetapi ini tidak sepenuhnya benar. Ini adalah pengetikan dinamis yang memungkinkan Anda menulis kode yang ekspresif dan ringkas . Analogi dengan layanan microser muncul di pikiran, ketika kita memotong monolith menjadi layanan microser, desain mengambil kekakuan tambahan, karena apa pun dapat dilakukan di dalam layanan mikro (PL, kerangka kerja, arsitektur), tetapi harus sesuai dengan antarmuka yang dinyatakan. Jadi di sini: ketika kami membagi proyek kami menjadi beberapa lapisan,Hubungan antara lapisan harus konsisten dengan kontrak , sementara di dalam lapisan, kontrak bersifat opsional. Jika tidak, Anda harus menyimpan konteks yang cukup besar di kepala Anda. Ingat, saya mengatakan bahwa masalah dengan kode buruk adalah dependensi, dan karenanya, tanpa antarmuka yang eksplisit, kami kembali meluncur ke tempat kami ingin pergi - dengan tidak adanya hubungan sebab akibat yang jelas .

repo RoomListUseCase, execute β€” . - , , . - . , , , repo .

Secara umum, pada waktu itu saya meninggalkan Arsitektur Bersih dalam proyek baru, sekali lagi menerapkan MVC klasik. Tetapi, setelah mengisi kumpulan kerucut berikutnya, ia kembali ke ide ini setahun kemudian, ketika, akhirnya, kami mulai meluncurkan layanan dengan Python 3.5+. Seperti yang Anda tahu, dia membawa anotasi tipe dan kelas data: Dua alat deskripsi antarmuka yang kuat. Berdasarkan pada mereka, saya membuat sketsa prototipe layanan, dan hasilnya sudah jauh lebih baik: lapisan berhenti berhamburan, terlepas dari kenyataan bahwa masih ada banyak kode, terutama ketika mengintegrasikan dengan kerangka kerja. Tetapi itu sudah cukup untuk mulai menerapkan pendekatan ini dalam proyek-proyek kecil. Secara bertahap, kerangka kerja mulai muncul yang berfokus pada penggunaan maksimum anotasi tipe: apistar (sekarang starlette), moltenframework. Bundel pydantic / FastAPI sekarang umum, dan integrasi dengan kerangka kerja seperti itu menjadi lebih mudah. Beginilah contoh dari restomatic / services.py di atas:

from typing import Optional, List
from pydantic import BaseModel

class Room(BaseModel):
   code: str
   size: int
   price: int
   latitude: float
   longitude: float

class RoomFilter(BaseModel):
   code: Optional[str] = None
   price_min: Optional[int] = None
   price_max: Optional[int] = None

class RoomStorage:
   def get_rooms(self, filters: RoomFilter) -> List[Room]: ...

class RoomListUseCase:
   def __init__(self, repo: RoomStorage):
       self.repo = repo
   def show_rooms(self, filters: RoomFilter) -> List[Room]:
       rooms = self.repo.get_rooms(filters=filters)
       return rooms

RoomListUseCase - kelas yang mengimplementasikan logika bisnis proyek. Anda seharusnya tidak memperhatikan fakta bahwa semua metode show_rooms tidak memanggil ke RoomStorage (saya tidak datang dengan contoh ini). Dalam kehidupan nyata, bisa juga ada perhitungan diskon, peringkat daftar berdasarkan iklan, dll. Namun, modul ini mandiri. Jika kita ingin menggunakan skenario ini di proyek lain, kita harus mengimplementasikan RoomStorage. Dan apa yang dibutuhkan untuk ini terlihat jelas langsung dari modul. Berbeda dengan contoh sebelumnya, lapisan seperti itu swasembada , dan ketika mengubahnya tidak perlu mengingat seluruh konteks. Dari dependensi non-sistemik hanya pydantic, mengapa, itu akan menjadi jelas dalam plug-in dari framework. Tidak ada ketergantungan, cara lain untuk meningkatkan keterbacaan kode, bukan konteks tambahan, bahkan pengembang pemula akan dapat memahami apa yang dilakukan modul ini.

Skenario logika bisnis tidak harus berupa kelas, di bawah ini adalah contoh skenario serupa dalam bentuk fungsi:

def rool_list_use_case(filters: RoomFilter, repo: RoomStorage) -> List[Room]:
   rooms = repo.get_rooms(filters=filters)
   return rooms


Dan inilah koneksi ke framework:

from typing import List
from fastapi import FastAPI, Depends
from rentomatic import services, adapters
app = FastAPI()

def get_use_case() -> services.RoomListUseCase:
   return services.RoomListUseCase(adapters.MemoryStorage())

@app.post("/rooms", response_model=List[services.Room])
def rooms(filters: services.RoomFilter, use_case=Depends(get_use_case)):
   return use_case.show_rooms(filters)

Menggunakan fungsi get_use_case, FastAPI mengimplementasikan pola Dependency Injection . Kita tidak perlu khawatir tentang serialisasi data, semua pekerjaan dilakukan oleh FastAPI bersamaan dengan pydantic. Sayangnya, data tidak selalu format logika bisnis yang cocok untuk siaran langsung di restoran <dan, sebaliknya, logika bisnis tidak tahu dari mana datanya - dengan garis miring, badan permintaan, cookie, dll . Dalam hal ini, isi fungsi ruangan akan memiliki konversi tertentu dari data input dan output, tetapi dalam kebanyakan kasus, jika kita bekerja dengan API, fungsi proxy yang mudah seperti itu sudah cukup. 

, , , RoomStorage. , 15 , , , .

Saya sengaja tidak memisahkan lapisan logika bisnis, seperti yang disarankan oleh model Arsitektur Bersih kanonik. Kelas Kamar seharusnya berada di lapisan wilayah domain Entitas yang mewakili wilayah domain, tetapi untuk contoh ini tidak perlu untuk ini. Dari menggabungkan lapisan Entity dan UseCase, proyek tidak berhenti menjadi implementasi Arsitektur Bersih. Robert Martin sendiri telah berulang kali mengatakan bahwa jumlah lapisan dapat bervariasi baik ke atas maupun ke bawah. Pada saat yang sama, proyek memenuhi kriteria utama Arsitektur Bersih:

  • Aturan penyeberangan perbatasan: model pydantic, yang pada dasarnya adalah DTO, lintas batas ;
  • Aturan Ketergantungan : lapisan logika bisnis tidak tergantung pada lapisan lain;
  • Aturan prioritas untuk lapisan dalam : itu adalah lapisan logika bisnis yang mendefinisikan antarmuka (RoomStorage), di mana logika bisnis berinteraksi dengan dunia luar.

Hari ini, beberapa proyek tim kami, diimplementasikan menggunakan pendekatan yang dijelaskan, sedang mengerjakan prod. Saya mencoba mengatur bahkan layanan terkecil dengan cara ini. Ini melatih dengan baik - pertanyaan yang belum pernah saya pikirkan sebelumnya. Misalnya, apa logika bisnis di sini? Ini jauh dari selalu jelas, misalnya, jika Anda menulis semacam proxy. Poin penting lainnya adalah belajar berpikir secara berbeda.. Ketika kita mendapatkan tugas, kita biasanya mulai berpikir tentang kerangka kerja, layanan yang digunakan, tentang apakah akan ada kebutuhan untuk garis di sini, di mana lebih baik untuk menyimpan data ini, yang bisa di-cache. Dalam pendekatan yang mendikte Arsitektur Bersih, pertama-tama kita harus mengimplementasikan logika bisnis dan baru kemudian beralih ke mengimplementasikan interaksi dengan infrastruktur, karena, menurut Robert Martin, tugas utama arsitektur adalah menunda momen ketika koneksi dengan Lapisan infrastruktur akan menjadi bagian integral dari aplikasi Anda.

Secara umum, saya melihat prospek yang menguntungkan untuk menggunakan Arsitektur Bersih dengan Python. Tetapi bentuknya, kemungkinan besar, akan sangat berbeda dari bagaimana itu diterima di PL lain. Selama beberapa tahun terakhir, saya telah melihat peningkatan minat yang signifikan dalam topik arsitektur di masyarakat. Jadi, pada PyCon terakhir ada beberapa laporan tentang penggunaan DDD, dan orang-orang dari laboratorium kering harus dicatat secara terpisah . Di perusahaan kami, banyak tim sudah menerapkan pendekatan yang dijelaskan pada tingkat tertentu. Kita semua melakukan hal yang sama, kita telah tumbuh, proyek kita telah berkembang, komunitas Python harus bekerja dengan ini, mendefinisikan gaya dan bahasa umum yang, misalnya, pernah menjadi untuk semua Django.

All Articles