Air Terjun Kompleks dan Arsitektur Sesuai Permintaan

Logo


Ketika berbicara tentang "kode buruk" orang hampir pasti berarti "kode kompleks" di antara masalah populer lainnya. Hal tentang kompleksitas adalah bahwa ia muncul entah dari mana. Suatu hari Anda memulai proyek yang cukup sederhana, hari lain Anda menemukannya dalam reruntuhan. Dan tidak ada yang tahu bagaimana dan kapan itu terjadi.


Tapi, ini akhirnya terjadi karena suatu alasan! Kompleksitas kode memasuki basis kode Anda dengan dua cara yang mungkin: dengan potongan besar dan tambahan tambahan. Dan orang-orang buruk dalam meninjau dan menemukan keduanya.


Ketika sejumlah besar kode masuk, peninjau akan ditantang untuk menemukan lokasi yang tepat di mana kode itu kompleks dan apa yang harus dilakukan. Kemudian, ulasan harus membuktikan poinnya: mengapa kode ini rumit sejak awal. Dan pengembang lain mungkin tidak setuju. Kita semua tahu jenis ulasan kode ini!


Jumlah baris untuk rasio ulasan dan komentar


Cara kerumitan kedua yang masuk ke kode Anda adalah penambahan tambahan: ketika Anda mengirimkan satu atau dua baris ke fungsi yang ada. Dan sangat sulit untuk mengetahui bahwa fungsi Anda baik-baik saja satu komit yang lalu, tetapi sekarang terlalu kompleks. Dibutuhkan sebagian besar konsentrasi, keterampilan meninjau, dan praktik navigasi kode yang baik untuk benar-benar melihatnya. Kebanyakan orang (seperti saya!) Tidak memiliki keterampilan ini dan memungkinkan kompleksitas untuk memasuki basis kode secara teratur.


Jadi, apa yang dapat dilakukan untuk mencegah kode Anda menjadi rumit? Kita perlu menggunakan otomatisasi! Mari kita selami kedalaman kode dan cara menemukan dan menyelesaikannya.


Dalam artikel ini, saya akan memandu Anda melalui tempat-tempat di mana kompleksitas hidup dan bagaimana cara untuk melawannya di sana. Kemudian kita akan membahas seberapa baik kode sederhana dan otomasi memungkinkan peluang gaya pengembangan "Continous Refactoring" dan "Architecture on Demand".


Kompleksitas dijelaskan


Orang mungkin bertanya: apa sebenarnya "kompleksitas kode" itu? Dan meskipun terdengar akrab, ada hambatan tersembunyi dalam memahami lokasi kompleksitas yang tepat. Mari kita mulai dengan bagian paling primitif dan kemudian pindah ke entitas tingkat yang lebih tinggi.


Ingat, bahwa artikel ini bernama "Air Terjun Kompleksitas"? Saya akan menunjukkan kepada Anda bagaimana kompleksitas dari primitif paling sederhana meluap ke abstraksi tertinggi.


Saya akan menggunakan python sebagai bahasa utama untuk contoh saya dan wemake-python-styleguide sebagai alat linting utama untuk menemukan pelanggaran dalam kode saya dan menggambarkan poin saya.


Ekspresi


Semua kode Anda terdiri dari ekspresi sederhana seperti a + 1 dan print(x) . Meskipun ekspresi itu sendiri sederhana, mereka mungkin meluap kode Anda dengan kerumitan di beberapa titik. Contoh: bayangkan Anda memiliki kamus yang mewakili beberapa model User dan Anda menggunakannya seperti ini:


 def format_username(user) -> str: if not user['username']: return user['email'] elif len(user['username']) > 12: return user['username'][:12] + '...' return '@' + user['username'] 

Terlihat sangat sederhana, bukan? Bahkan, itu berisi dua masalah kompleksitas berbasis ekspresi. Itu terlalu sering overuses 'username' string overuses 'username' dan menggunakan angka ajaib 12 (mengapa kita menggunakan nomor ini di tempat pertama, mengapa tidak 13 atau 10 ?). Sulit untuk menemukan hal-hal semacam ini sendirian. Begini tampilan versi yang lebih baik:


 #: That's how many chars fit in the preview box. LENGTH_LIMIT: Final = 12 def format_username(user) -> str: username = user['username'] if not username: return user['email'] elif len(username) > LENGTH_LIMIT: # See? It is now documented return username[:LENGTH_LIMIT] + '...' return '@' + username 

Ada berbagai masalah dengan ekspresi juga. Kami juga dapat memiliki ekspresi yang terlalu sering digunakan : ketika Anda menggunakan atribut some_object.some_attr mana saja alih-alih membuat variabel lokal baru. Kami juga dapat memiliki kondisi logika yang terlalu kompleks atau akses dot yang terlalu dalam .


Solusi : buat variabel, argumen, atau konstanta baru. Buat dan gunakan fungsi atau metode utilitas baru jika Anda harus.


Garis


Ekspresi dari baris kode (tolong, jangan bingung baris dengan pernyataan: pernyataan tunggal dapat mengambil beberapa baris dan beberapa pernyataan mungkin terletak pada satu baris).


Metrik kompleksitas pertama dan paling jelas untuk sebuah garis adalah panjangnya. Ya, Anda mendengarnya dengan benar. Itu sebabnya kami (programmer) lebih suka tetap berpegang pada aturan 80 karakter per baris dan bukan karena sebelumnya digunakan dalam teletypewriter. Ada banyak desas-desus tentang hal itu akhir-akhir ini, mengatakan bahwa tidak masuk akal untuk menggunakan 80 karakter untuk kode Anda dalam 2k19. Tapi, itu jelas tidak benar.


Idenya sederhana. Anda dapat memiliki logika dua kali lebih banyak dalam satu baris dengan 160 karakter dibandingkan dengan hanya 80 karakter. Itu sebabnya batas ini harus ditetapkan dan ditegakkan. Ingat, ini bukan pilihan gaya . Ini adalah metrik kompleksitas!


Metrik kompleksitas jalur utama kedua kurang dikenal dan kurang digunakan. Ini disebut Kompleksitas Jones . Gagasan di baliknya sederhana: kita menghitung node kode (atau ast ) dalam satu baris untuk mendapatkan kompleksitasnya. Mari kita lihat contohnya. Kedua garis ini pada dasarnya berbeda dalam hal kompleksitas tetapi memiliki lebar yang sama persis dalam karakter:


 print(first_long_name_with_meaning, second_very_long_name_with_meaning, third) print(first * 5 + math.pi * 2, matrix.trans(*matrix), display.show(matrix, 2)) 

Mari kita hitung simpul di yang pertama: satu panggilan, tiga nama. Empat node secara total. Yang kedua memiliki dua puluh satu ast node. Nah, perbedaannya jelas. Itu sebabnya kami menggunakan metrik Kompleksitas Jones untuk memungkinkan garis panjang pertama dan melarang yang kedua berdasarkan kompleksitas internal, bukan hanya pada panjang mentah.


Apa yang harus dilakukan dengan garis dengan skor Kompleksitas Jones yang tinggi?


Solusi : Bagi mereka menjadi beberapa baris atau buat variabel perantara baru, fungsi utilitas, kelas baru, dll.


 print( first * 5 + math.pi * 2, matrix.trans(*matrix), display.show(matrix, 2), ) 

Sekarang cara ini lebih mudah dibaca!


Struktur


Langkah selanjutnya adalah menganalisis struktur bahasa seperti if , for , with , dll yang dibentuk dari garis dan ekspresi. Saya harus mengatakan bahwa poin ini sangat spesifik bahasa. Saya akan menampilkan beberapa aturan dari kategori ini menggunakan python juga.


Kami akan mulai dengan if . Apa yang bisa lebih mudah daripada orang tua yang baik if ? Sebenarnya, if mulai rumit sangat cepat. Berikut adalah contoh bagaimana seseorang dapat reimplement switch dengan if :


 if isinstance(some, int): ... elif isinstance(some, float): ... elif isinstance(some, complex): ... elif isinstance(some, str): ... elif isinstance(some, bytes): ... elif isinstance(some, list): ... 

Apa masalah dengan kode ini? Nah, bayangkan bahwa kita memiliki puluhan tipe data yang harus dicakup termasuk data bea cukai yang belum kita sadari. Maka kode kompleks ini adalah indikator bahwa kita memilih pola yang salah di sini. Kita perlu memperbaiki kode kita untuk memperbaiki masalah ini. Misalnya, seseorang dapat menggunakan typeclass es atau typeclass . Mereka pekerjaan yang sama, tetapi lebih baik.


python tidak pernah berhenti untuk menghibur kita. Misalnya, Anda dapat menulis with jumlah kasus yang sewenang-wenang , yang terlalu rumit secara mental dan membingungkan:


 with first(), second(), third(), fourth(): ... 

Anda juga dapat menulis pemahaman dengan sejumlah if dan for ekspresi, yang dapat menyebabkan kode yang kompleks dan tidak dapat dibaca:


 [ (x, y, z) for x in x_coords for y in y_coords for z in z_coords if x > 0 if y > 0 if z > 0 if x + y <= z if x + z <= y if y + z <= x ] 

Bandingkan dengan versi yang sederhana dan mudah dibaca:


 [ (x, y, z) for x, y, x in itertools.product(x_coords, y_coords, z_coords) if valid_coordinates(x, y, z) ] 

Anda juga dapat secara tidak sengaja memasukkan multiple statements inside a try , yang tidak aman karena dapat meningkatkan dan menangani pengecualian di tempat yang diharapkan:


 try: user = fetch_user() # Can also fail, but don't expect that log.save_user_operation(user.email) # Can fail, and we know it except MyCustomException as exc: ... 

Dan itu bahkan tidak 10% dari kasus yang dapat dan akan salah dengan kode python Anda. Ada banyak, lebih banyak lagi kasus tepi yang harus dilacak dan dianalisis.


Solusi : Satu-satunya solusi yang mungkin adalah menggunakan linter yang bagus untuk bahasa pilihan Anda. Dan refactor tempat kompleks yang menyoroti ini. Jika tidak, Anda harus menemukan kembali roda dan menetapkan kebijakan khusus untuk masalah yang sama persis.


Fungsi


Ekspresi, pernyataan, dan struktur membentuk fungsi. Kompleksitas dari entitas ini mengalir ke fungsi. Dan di situlah hal-hal mulai menarik. Karena fungsi memiliki lusinan metrik kompleksitas: baik dan buruk.


Kita akan mulai dengan yang paling dikenal: kompleksitas siklomatik dan panjang fungsi diukur dalam baris kode. Kompleksitas siklus menunjukkan berapa banyak putaran yang dapat dilakukan oleh alur eksekusi Anda: hampir sama dengan jumlah unit test yang diperlukan untuk sepenuhnya mencakup kode sumber. Ini adalah metrik yang baik karena menghormati semantik dan membantu pengembang untuk melakukan refactoring. Di sisi lain, panjang fungsi adalah metrik yang buruk. Itu tidak cocok dengan metrik Kompleksitas Jones yang telah dijelaskan sebelumnya karena kita sudah tahu: banyak baris lebih mudah dibaca daripada satu baris besar dengan semua yang ada di dalamnya. Kami akan berkonsentrasi hanya pada metrik yang baik dan mengabaikan yang buruk.


Berdasarkan pengalaman saya, beberapa metrik kompleksitas yang berguna harus dihitung alih-alih panjang fungsi reguler:


  • Jumlah dekorator fungsi; lebih rendah lebih baik
  • Jumlah argumen; lebih rendah lebih baik
  • Jumlah anotasi; lebih tinggi lebih baik
  • Jumlah variabel lokal; lebih rendah lebih baik
  • Jumlah pengembalian, hasil, menunggu; lebih rendah lebih baik
  • Jumlah pernyataan dan ekspresi; lebih rendah lebih baik

Kombinasi dari semua pemeriksaan ini benar-benar memungkinkan Anda untuk menulis fungsi sederhana (semua aturan juga berlaku untuk metode).


Ketika Anda akan mencoba melakukan beberapa hal buruk dengan fungsi Anda, Anda pasti akan mematahkan setidaknya satu metrik. Dan ini akan mengecewakan linter kami dan menghancurkan build Anda. Akibatnya, fungsi Anda akan disimpan.


Solusi : ketika satu fungsi terlalu kompleks, satu-satunya solusi yang Anda miliki adalah dengan membagi fungsi ini menjadi beberapa fungsi.


Kelas


Level abstraksi berikutnya setelah fungsi adalah kelas. Dan seperti yang sudah Anda duga mereka bahkan lebih kompleks dan cair daripada fungsi. Karena kelas mungkin berisi beberapa fungsi di dalam (yang disebut metode) dan memiliki fitur unik lainnya seperti warisan dan mixin, atribut tingkat kelas dan dekorator tingkat kelas. Jadi, kita harus memeriksa semua metode sebagai fungsi dan tubuh kelas itu sendiri.


Untuk kelas kita harus mengukur metrik berikut:


  • Jumlah dekorator tingkat kelas; lebih rendah lebih baik
  • Jumlah kelas dasar; lebih rendah lebih baik
  • Jumlah atribut publik tingkat kelas; lebih rendah lebih baik
  • Jumlah atribut publik tingkat instance; lebih rendah lebih baik
  • Jumlah metode; lebih rendah lebih baik

Ketika salah satu dari ini terlalu rumit - kita harus membunyikan alarm dan gagal membangun!


Solusi : refactor kelas gagal Anda! Membagi satu kelas kompleks yang ada menjadi beberapa kelas sederhana atau membuat fungsi utilitas baru dan menggunakan komposisi.


Disebutkan: seseorang juga dapat melacak metrik kohesi dan kopling untuk memvalidasi kompleksitas desain OOP Anda.


Modul


Modul memang berisi banyak pernyataan, fungsi, dan kelas. Dan seperti yang mungkin telah Anda sebutkan, kami biasanya menyarankan untuk membagi fungsi dan kelas menjadi yang baru. Itu sebabnya kita harus mengawasi kompleksitas modul: itu benar-benar mengalir ke modul dari kelas dan fungsi.


Untuk menganalisis kompleksitas modul kita harus memeriksa:


  • Jumlah impor dan nama yang diimpor; lebih rendah lebih baik
  • Jumlah kelas dan fungsi; lebih rendah lebih baik
  • Kompleksitas rata-rata fungsi dan kelas di dalamnya; lebih rendah lebih baik

Apa yang kita lakukan dalam modul yang kompleks?


Solusi : ya, Anda benar. Kami membagi satu modul menjadi beberapa modul.


Paket


Paket berisi beberapa modul. Untungnya, hanya itu yang mereka lakukan.


Jadi, sejumlah modul dalam satu paket dapat segera mulai menjadi terlalu besar, sehingga Anda akan berakhir dengan terlalu banyak modul. Dan itu adalah satu-satunya kompleksitas yang dapat ditemukan dengan paket.


Solusi : Anda harus membagi paket menjadi sub-paket dan paket dari berbagai tingkatan.


Efek air terjun yang kompleks


Kami sekarang telah membahas hampir semua jenis abstraksi yang mungkin ada dalam basis kode Anda. Apa yang telah kita pelajari darinya? Takeaway utama, untuk saat ini, adalah bahwa sebagian besar masalah dapat diselesaikan dengan mengeluarkan kompleksitas ke tingkat abstraksi yang sama atau atas.


Air terjun yang kompleks


Ini membawa kita pada ide terpenting dari artikel ini: jangan biarkan kode Anda dipenuhi dengan kerumitan. Saya akan memberikan beberapa contoh bagaimana biasanya terjadi.


Bayangkan Anda sedang mengimplementasikan fitur baru. Dan itulah satu-satunya perubahan yang Anda lakukan:


 +++ if user.is_active and user.has_sub() and sub.is_due(tz.now() + delta): --- if user.is_active and user.has_sub(): 

Terlihat ok, saya akan memberikan kode ini saat ditinjau. Dan tidak ada hal buruk yang akan terjadi. Tapi, poin yang saya lewatkan adalah bahwa kompleksitas meluap garis ini! Itulah yang akan wemake-python-styleguide oleh wemake-python-styleguide :


wemake-python-styleguide-output


Ok, kita sekarang harus menyelesaikan kompleksitas ini. Mari kita membuat variabel baru:


 class Product(object): ... def can_be_purchased(self, user_id) -> bool: ... is_sub_paid = sub.is_due(tz.now() + delta) if user.is_active and user.has_sub() and is_sub_paid: ... ... ... 

Sekarang, kompleksitas garis terpecahkan. Tapi tunggu sebentar. Bagaimana jika fungsi kita memiliki terlalu banyak variabel sekarang? Karena kami telah membuat variabel baru tanpa memeriksa nomor mereka di dalam fungsi terlebih dahulu. Dalam hal ini kita harus membagi metode ini menjadi beberapa yang seperti ini:


 class Product(object): ... def can_be_purchased(self, user_id) -> bool: ... if self._has_paid_sub(user, sub, delta): ... ... def _has_paid_sub(self, user, sub, delta) -> bool: is_sub_paid = sub.is_due(tz.now() + delta) return user.is_active and user.has_sub() and is_sub_paid ... 

Sekarang kita selesai! Benar? Tidak, karena kita sekarang harus memeriksa kompleksitas kelas Product . Bayangkan, itu sekarang memiliki terlalu banyak metode karena kita telah membuat _has_paid_sub baru.


Ok, kita jalankan linter kita untuk memeriksa kompleksitasnya lagi. Dan ternyata kelas Product kami memang terlalu rumit saat ini. Tindakan kita? Kami membaginya menjadi beberapa kelas!


 class Policy(object): ... class SubcsriptionPolicy(Policy): ... def can_be_purchased(self, user_id) -> bool: ... if self._has_paid_sub(user, sub, delta): ... ... def _has_paid_sub(self, user, sub, delta) -> bool: is_sub_paid = sub.is_due(tz.now() + delta) return user.is_active and user.has_sub() and is_sub_paid class Product(object): _purchasing_policy: Policy ... ... 

Tolong beritahu saya bahwa itu adalah iterasi terakhir! Maaf, tapi sekarang kita harus memeriksa kompleksitas modul. Dan coba tebak? Kami sekarang memiliki terlalu banyak anggota modul. Jadi, kita harus membagi modul menjadi yang terpisah! Kemudian kami memeriksa kompleksitas paket. Dan juga mungkin membaginya menjadi beberapa sub paket.


Pernahkah Anda melihatnya? Karena aturan kompleksitas yang terdefinisi dengan baik, modifikasi single-line kami ternyata menjadi sesi refactoring besar dengan beberapa modul dan kelas baru. Dan kami sendiri belum membuat keputusan: semua tujuan refactoring kami didorong oleh kompleksitas internal dan orang-orang yang mengungkapkannya.


Itulah yang saya sebut proses "Continuous Refactoring". Anda terpaksa melakukan refactoring. Selalu.


Proses ini juga memiliki satu konsekuensi menarik. Ini memungkinkan Anda untuk memiliki "Architecture on Demand". Biarkan saya jelaskan. Dengan filosofi "Arsitektur Sesuai Permintaan" Anda selalu memulai dari yang kecil. Misalnya dengan satu file logic/domains/user.py Dan Anda mulai meletakkan semua yang terkait User sana. Karena pada saat ini Anda mungkin tidak tahu seperti apa arsitektur Anda nantinya. Dan kamu tidak peduli. Anda hanya memiliki tiga fungsi.


Beberapa orang jatuh ke dalam perangkap kompleksitas arsitektur vs kode. Mereka dapat terlalu menyulitkan arsitektur mereka dari awal dengan lapisan repositori / layanan / domain penuh. Atau mereka dapat terlalu memperumit kode sumber tanpa pemisahan yang jelas. Berjuang dan hidup seperti ini selama bertahun-tahun (jika mereka akan bisa hidup selama bertahun-tahun dengan kode seperti ini!).


Konsep "Architecture on Demand" memecahkan masalah ini. Anda memulai dari yang kecil, ketika saatnya tiba - Anda membelah dan memperbaiki hal-hal:


  1. Anda mulai dengan logic/domains/user.py dan meletakkan semuanya di sana
  2. Kemudian Anda membuat logic/domains/user/repository.py ketika Anda memiliki hal-hal terkait database yang cukup
  3. Kemudian Anda membaginya menjadi logic/domains/user/repository/queries.py dan logic/domains/user/repository/commands.py ketika kerumitan memberitahu Anda untuk melakukannya
  4. Kemudian Anda membuat logic/domains/user/services.py dengan hal-hal terkait http
  5. Kemudian Anda membuat modul baru yang disebut logic/domains/order.py
  6. Dan seterusnya dan seterusnya

Itu dia. Ini adalah alat yang sempurna untuk menyeimbangkan arsitektur dan kompleksitas kode Anda. Dan dapatkan sebanyak mungkin arsitektur yang Anda butuhkan saat ini.


Kesimpulan


Linter yang baik tidak hanya menemukan koma yang hilang dan kutipan yang buruk. Linter yang baik memungkinkan Anda untuk bergantung pada itu dengan keputusan arsitektur dan membantu Anda dengan proses refactoring.


Misalnya, wemake-python-styleguide mungkin membantu Anda dengan kompleksitas kode sumber python , ini memungkinkan Anda untuk:


  • Berhasil melawan kompleksitas di semua level
  • Menegakkan sejumlah besar standar penamaan, praktik terbaik, dan pemeriksaan konsistensi
  • Mudah mengintegrasikannya ke basis kode lama dengan bantuan opsi diff atau alat flakehell , sehingga pelanggaran lama akan dimaafkan, tetapi yang baru tidak akan diizinkan
  • Aktifkan ke [CI] () Anda, bahkan sebagai Tindakan Github

Jangan biarkan kompleksitas meluap kode Anda, gunakan linter yang bagus !

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


All Articles