Latar belakang
Selama beberapa tahun terakhir, saya telah berpartisipasi dalam sejumlah besar wawancara. Pada masing-masing dari mereka saya bertanya kepada pelamar tentang prinsip tanggung jawab tunggal (selanjutnya disebut SRP). Dan kebanyakan orang tidak tahu apa-apa tentang prinsip itu. Dan bahkan dari mereka yang bisa membaca definisi tersebut, hampir tidak ada yang bisa mengatakan bagaimana mereka menggunakan prinsip ini dalam pekerjaan mereka. Mereka tidak bisa mengatakan bagaimana SRP mempengaruhi kode yang mereka tulis atau ulasan kode rekan. Beberapa dari mereka juga memiliki kesalahpahaman bahwa SRP, seperti SOLID keseluruhan, hanya relevan untuk pemrograman berorientasi objek. Juga, seringkali orang tidak dapat mengidentifikasi kasus-kasus pelanggaran prinsip ini, hanya karena kode ditulis dengan gaya yang direkomendasikan oleh kerangka kerja yang terkenal.
Redux adalah contoh utama dari kerangka kerja yang pedomannya melanggar SRP.
Masalah SRP
Saya ingin memulai dengan nilai prinsip ini, dengan manfaat yang diberikannya. Dan saya juga ingin mencatat bahwa prinsip ini berlaku tidak hanya untuk OOP, tetapi juga untuk pemrograman prosedural, fungsional dan bahkan deklaratif. HTML, sebagai perwakilan dari yang terakhir, dapat dan juga harus didekomposisi, terutama sekarang ketika dikendalikan oleh kerangka kerja UI seperti React atau Angular. Selain itu, prinsip ini berlaku untuk bidang teknik lainnya. Dan tidak hanya rekayasa, ada ekspresi seperti itu dalam mata pelajaran militer: "memecah belah dan menaklukkan", yang pada umumnya merupakan perwujudan dari prinsip yang sama. Kompleksitas membunuh, membaginya menjadi beberapa bagian dan Anda akan menang.
Mengenai bidang teknik lainnya, di sini di hub, ada artikel yang menarik tentang bagaimana pesawat yang dikembangkan mesin gagal, tidak beralih untuk mundur atas perintah pilot. Masalahnya adalah mereka salah menafsirkan keadaan sasis. Alih-alih mengandalkan sistem yang mengendalikan sasis, pengontrol mesin langsung membaca sensor, sakelar batas, dll. Yang terletak di sasis. Itu juga disebutkan dalam artikel bahwa mesin harus menjalani sertifikasi panjang sebelum itu bahkan memakai pesawat prototipe. Dan pelanggaran SRP dalam kasus ini jelas mengarah pada fakta bahwa ketika mengubah desain sasis, kode pada pengontrol mesin perlu dimodifikasi dan disertifikasi ulang. Lebih buruk lagi, pelanggaran terhadap prinsip ini hampir sepadan dengan pesawat dan kehidupan pilot. Untungnya, pemrograman kita sehari-hari tidak mengancam konsekuensi seperti itu, namun, Anda tetap tidak boleh mengabaikan prinsip-prinsip penulisan kode yang baik. Dan inilah alasannya:
- Dekomposisi kode mengurangi kompleksitasnya. Misalnya, jika solusi untuk masalah mengharuskan Anda untuk menulis kode dengan kompleksitas siklomatik empat, maka metode yang bertanggung jawab untuk menyelesaikan dua masalah tersebut pada saat yang sama akan membutuhkan kode dengan kompleksitas 16. Jika dibagi menjadi dua metode, maka total kompleksitas akan menjadi 8. Tentu saja, ini tidak selalu turun ke jumlah terhadap pekerjaan, tetapi tren akan kira-kira sama pula.
- Unit testing kode terurai disederhanakan dan lebih efisien.
- Kode terurai menciptakan lebih sedikit resistensi untuk berubah. Saat melakukan perubahan, kecil kemungkinannya untuk melakukan kesalahan.
- Kode semakin terstruktur. Mencari sesuatu dalam kode yang diatur dalam file dan folder jauh lebih mudah daripada di satu sepatu besar.
- Pemisahan kode boilerplate dari logika bisnis mengarah pada fakta bahwa pembuatan kode dapat diterapkan dalam suatu proyek.
Dan semua tanda ini berjalan bersamaan, ini adalah tanda-tanda kode yang sama. Anda tidak harus memilih antara, misalnya, kode yang telah diuji dengan baik dan kode yang terstruktur dengan baik.
Definisi yang ada tidak berfungsi
Salah satu definisi adalah: "seharusnya hanya ada satu alasan untuk mengubah kode (kelas atau fungsi)". Masalah dengan definisi ini adalah bahwa itu bertentangan dengan prinsip Open-Close, yang kedua dari kelompok prinsip SOLID. Definisi: "kode harus terbuka untuk ekstensi dan ditutup untuk perubahan." Satu alasan untuk perubahan versus larangan total terhadap perubahan. Jika kita mengungkapkan secara lebih terperinci apa yang dimaksud di sini, ternyata tidak ada konflik antara prinsip-prinsip tersebut, tetapi pasti ada konflik antara definisi fuzzy.
Definisi kedua, yang lebih langsung adalah: "kode hanya memiliki satu tanggung jawab." Masalah dengan definisi ini adalah sifat manusia untuk menggeneralisasi segalanya.
Misalnya, ada peternakan yang menanam ayam, dan pada saat itu peternakan hanya memiliki satu tanggung jawab. Maka keputusan dibuat untuk membiakkan bebek di sana juga. Secara naluriah, kita akan menyebutnya sebagai peternakan unggas, daripada mengakui bahwa sekarang ada dua tanggung jawab. Tambahkan domba di sana, dan sekarang ini adalah peternakan hewan peliharaan. Kemudian kami ingin menanam tomat atau jamur di sana, dan menghasilkan nama yang lebih umum berikut. Hal yang sama berlaku untuk "satu alasan" untuk perubahan. Alasan ini dapat digeneralisasikan dengan imajinasi yang mencukupi.
Contoh lain adalah kelas manajer stasiun ruang angkasa. Dia tidak melakukan apa-apa lagi, dia hanya mengelola stasiun ruang angkasa. Bagaimana Anda menyukai kelas ini dengan satu tanggung jawab?
Dan, karena saya menyebutkan Redux ketika pelamar pekerjaan mengetahui teknologi ini, saya juga mengajukan pertanyaan, apakah peredam SRP tipikal melanggar?
Reducer, saya ingat, termasuk pernyataan switch, dan kebetulan itu tumbuh hingga puluhan atau bahkan ratusan kasus. Dan satu-satunya tanggung jawab peredam adalah mengelola transisi status aplikasi Anda. Itulah, secara harfiah, beberapa pelamar menjawab. Dan tidak ada petunjuk yang bisa menghilangkan pendapat ini.
Secara total, jika beberapa jenis kode tampaknya memenuhi prinsip SRP, tetapi pada saat yang sama baunya tidak menyenangkan - tahu mengapa ini terjadi. Karena definisi "kode harus memiliki satu tanggung jawab" sama sekali tidak berfungsi.
Definisi yang lebih tepat
Dari percobaan dan kesalahan, saya memiliki definisi yang lebih baik:
Kode Tanggung Jawab Seharusnya Tidak Terlalu BesarYa, sekarang Anda perlu "mengukur" tanggung jawab kelas atau fungsi. Dan jika itu terlalu besar, maka Anda perlu membagi tanggung jawab besar ini menjadi beberapa tanggung jawab yang lebih kecil. Kembali ke contoh peternakan, bahkan tanggung jawab untuk membesarkan ayam bisa terlalu besar dan masuk akal untuk memisahkan ayam broiler dari ayam petelur, misalnya.
Tetapi bagaimana mengukurnya, bagaimana menentukan bahwa tanggung jawab kode ini terlalu besar?
Sayangnya, saya tidak memiliki metode yang akurat secara matematis, hanya metode empiris. Dan sebagian besar dari semua ini datang dengan pengalaman, pengembang pemula sama sekali tidak dapat menguraikan kode, yang lebih maju lebih baik dalam memilikinya, meskipun mereka tidak selalu dapat menjelaskan mengapa mereka melakukannya dan bagaimana hal itu jatuh pada teori seperti SRP.
- Kompleksitas siklomatik metrik. Sayangnya, ada beberapa cara untuk menutupi metrik ini, tetapi jika Anda mengumpulkannya, maka ada kemungkinan ia akan menunjukkan tempat paling rentan dalam aplikasi Anda.
- Ukuran fungsi dan kelas. Fungsi 800-line tidak perlu dibaca untuk memahami bahwa ada sesuatu yang salah dengannya.
- Banyak impor. Suatu kali saya membuka file dalam proyek tim tetangga dan melihat seluruh layar impor, menekan halaman ke bawah dan lagi hanya ada impor di layar. Hanya setelah pers kedua saya melihat awal kode. Anda dapat mengatakan bahwa semua IDE modern dapat menyembunyikan impor di bawah "tanda plus", tetapi saya mengatakan bahwa kode yang baik tidak perlu menyembunyikan "bau". Selain itu, saya perlu menggunakan kembali sepotong kecil kode dan saya menghapusnya dari file ini ke yang lain, dan seperempat atau bahkan sepertiga dari impor bergerak di belakang bagian ini. Kode ini jelas bukan milik di sana.
- Tes unit. Jika Anda masih kesulitan menentukan jumlah tanggung jawab, paksa diri Anda untuk menulis tes. Jika Anda perlu menulis dua lusin tes pada tujuan utama suatu fungsi, tidak menghitung kasus batas, dll, maka diperlukan dekomposisi.
- Hal yang sama berlaku untuk terlalu banyak langkah persiapan di awal tes dan pemeriksaan di akhir. Di Internet, omong-omong, Anda dapat menemukan pernyataan utopis yang disebut Seharusnya hanya ada satu penegasan dalam ujian. Saya percaya bahwa ide bagus yang sewenang-wenang, yang dibesarkan secara absolut, dapat menjadi sangat tidak praktis.
- Logika bisnis tidak boleh secara langsung bergantung pada alat eksternal. Driver Oracle, rute Express, diinginkan untuk memisahkan semua ini dari logika bisnis dan / atau bersembunyi di balik antarmuka.
Beberapa poin:
Tentu saja, seperti yang telah saya sebutkan, ada sisi lain dari koin, dan 800 metode pada satu baris mungkin tidak lebih baik dari satu metode pada 800 baris, harus ada keseimbangan dalam segala hal.
Yang kedua - saya tidak membahas pertanyaan di mana harus meletakkan kode ini atau itu sesuai dengan tanggung jawabnya. Sebagai contoh, kadang-kadang pengembang juga mengalami kesulitan dengan menarik terlalu banyak logika ke dalam lapisan DAL.
Ketiga, saya tidak mengusulkan batasan keras tertentu seperti "tidak lebih dari 50 baris per fungsi". Pendekatan ini hanya melibatkan arahan untuk pengembangan pengembang, dan mungkin tim. Dia bekerja untuk saya, dia harus mendapatkan uang untuk orang lain.
Dan akhirnya, jika Anda melewati TDD, ini saja sudah pasti akan membuat Anda menguraikan kode jauh sebelum Anda menulis 20 tes dengan masing-masing 20 pernyataan.
Memisahkan logika bisnis dari kode boilerplate
Berbicara tentang aturan kode yang baik, Anda tidak dapat melakukannya tanpa contoh. Contoh pertama adalah tentang memisahkan kode boilerplate.

Contoh ini menunjukkan bagaimana kode back-end biasanya ditulis. Orang-orang biasanya menulis logika yang tidak dapat dipisahkan dengan kode yang menunjukkan parameter ke server Web Express seperti URL, metode permintaan, dll.
Saya menandai logika bisnis sebagai penanda hijau, dan kode diselingi merah yang berinteraksi dengan parameter kueri (merah).
Saya selalu berbagi dua tanggung jawab ini dengan cara ini:

Dalam contoh ini, semua interaksi dengan Express ada dalam file terpisah.
Sekilas, mungkin tampak bahwa contoh kedua tidak membawa perbaikan, ada 2 file, bukan satu, baris tambahan muncul, yang belum ada sebelumnya - nama kelas dan metode tanda tangan. Lalu apa yang diberikan oleh pemisahan kode ini? Pertama-tama, "titik masuk aplikasi" tidak lagi Ekspres. Sekarang ini adalah fungsi jenis huruf biasa. Atau fungsi javascript, apakah C #, yang menulis WebAPI tentang apa.
Ini, pada gilirannya, memungkinkan Anda untuk melakukan berbagai tindakan yang tidak tersedia pada contoh pertama. Misalnya, Anda dapat menulis tes perilaku tanpa harus menaikkan Express, tanpa menggunakan permintaan http di dalam tes. Dan bahkan tidak perlu melakukan segala macam pembasahan, ganti objek Router dengan objek "test" Anda, sekarang kode aplikasi hanya dapat dipanggil langsung dari tes.
Fitur menarik lainnya yang disediakan oleh dekomposisi ini adalah bahwa Anda sekarang dapat menulis pembuat kode yang akan mem-parsing userApiService dan menghasilkan kode berdasarkan basisnya yang menghubungkan layanan ini dengan Express. Dalam publikasi masa depan saya, saya berencana untuk menunjukkan yang berikut: pembuatan kode tidak akan menghemat waktu dalam proses penulisan kode. Biaya pembuat kode tidak akan terbayar dengan fakta bahwa sekarang Anda tidak perlu menyalin pelat baja ini. Pembuatan kode akan terbayar dengan fakta bahwa kode yang dihasilkannya tidak memerlukan dukungan, yang akan menghemat waktu dan, yang paling penting, saraf pengembang dalam jangka panjang.
Bagilah dan taklukkan
Metode penulisan kode ini sudah ada sejak lama, saya tidak menemukannya sendiri. Saya baru saja sampai pada kesimpulan bahwa sangat nyaman ketika menulis logika bisnis. Dan untuk ini, saya datang dengan contoh fiktif lain, menunjukkan bagaimana Anda dapat dengan cepat dan mudah menulis kode yang segera terurai dengan baik dan juga didokumentasikan sendiri dengan metode penamaan.
Katakanlah Anda mendapat tugas dari analis bisnis untuk membuat metode yang mengirimkan laporan karyawan ke perusahaan asuransi. Untuk melakukan ini:
- Data harus diambil dari database
- Konversi ke format yang diinginkan
- Kirim laporan yang dihasilkan
Persyaratan semacam itu tidak selalu ditulis secara eksplisit, kadang-kadang urutan seperti itu dapat tersirat atau diklarifikasi dari percakapan dengan analis. Dalam proses penerapan metode ini, jangan buru-buru membuka koneksi ke database atau jaringan, sebagai gantinya cobalah menerjemahkan algoritma sederhana ini ke dalam kode "apa adanya". Sesuatu seperti ini:
async function sendEmployeeReportToProvider(reportId){ const data = await dal.getEmployeeReportData(reportId); const formatted = reportDataService.prepareEmployeeReport(data); await networkService.sendReport(formatted); }
Dengan pendekatan ini, ternyata menjadi kode yang cukup sederhana, mudah dibaca dan diuji, meskipun saya percaya bahwa kode ini sepele dan tidak perlu pengujian. Dan itu adalah tanggung jawab metode ini untuk tidak mengirim laporan, tanggung jawabnya adalah untuk membagi tugas yang kompleks ini menjadi tiga subtugas.
Selanjutnya, kami kembali ke persyaratan dan mengetahui bahwa laporan harus terdiri dari bagian gaji dan bagian dengan jam kerja.
function prepareEmployeeReport(reportData){ const salarySection = prepareSalarySection(reportData); const workHoursSection = prepareWorkHoursSection(reportData); return { salarySection, workHoursSection }; }
Dan seterusnya dan seterusnya kami terus memecah tugas sampai implementasi metode kecil yang dekat dengan sisa-sisa sepele.
Interaksi dengan Prinsip Buka-Tutup
Di awal artikel saya mengatakan bahwa definisi prinsip-prinsip SRP dan Open-Close saling bertentangan. Yang pertama mengatakan bahwa harus ada satu alasan untuk perubahan, yang kedua mengatakan bahwa kode harus ditutup untuk perubahan. Dan prinsip-prinsip itu sendiri, tidak hanya tidak saling bertentangan, sebaliknya, mereka bekerja secara sinergis satu sama lain. Semua 5 prinsip SOLID ditujukan pada satu tujuan yang baik - untuk memberi tahu pengembang kode mana yang "buruk" dan bagaimana mengubahnya sehingga menjadi "baik". Ironisnya - saya baru saja mengganti 5 tanggung jawab dengan satu lagi tanggung jawab.
Jadi, di samping contoh sebelumnya dengan mengirimkan laporan ke perusahaan asuransi, bayangkan bahwa seorang analis bisnis mendatangi kami dan mengatakan bahwa sekarang kita perlu menambahkan fungsionalitas kedua ke proyek. Laporan yang sama harus dicetak.
Bayangkan ada pengembang yang percaya bahwa SRP "bukan tentang dekomposisi."
Dengan demikian, prinsip ini tidak menunjukkan kepadanya perlunya pembusukan, dan ia menyadari seluruh tugas pertama dalam satu fungsi. Setelah tugas datang kepadanya, ia menggabungkan dua tanggung jawab menjadi satu, karena mereka memiliki banyak kesamaan dan menggeneralisasi namanya. Sekarang tanggung jawab ini disebut "laporan layanan." Implementasi ini terlihat seperti ini:
async function serveEmployeeReportToProvider(reportId, serveMethod){ switch(serveMethod) { case sendToProvider: case print: default: throw; } }
Mengingatkan beberapa kode dalam proyek Anda? Seperti yang saya katakan, kedua definisi langsung SRP tidak berfungsi. Mereka tidak mengirimkan informasi kepada pengembang bahwa kode tersebut tidak dapat ditulis. Dan kode apa yang bisa ditulis. Masih ada satu alasan bagi pengembang untuk mengubah kode ini. Dia hanya menyebut alasan sebelumnya, menambahkan saklar dan tenang. Dan di sini prinsip prinsip Open-Close datang ke tempat kejadian, yang secara langsung mengatakan bahwa tidak mungkin untuk memodifikasi file yang ada. Itu perlu untuk menulis kode sehingga ketika menambahkan fungsionalitas baru perlu untuk menambahkan file baru, dan tidak mengedit yang sudah ada. Artinya, kode semacam itu buruk dari sudut pandang dua prinsip sekaligus. Dan jika yang pertama tidak membantu melihatnya, yang kedua akan membantu.
Dan bagaimana metode membagi dan menaklukkan memecahkan masalah yang sama:
async function printEmployeeReport(reportId){ const data = await dal.getEmployeeReportData(reportId); const formatted = reportDataService.prepareEmployeeReport(data); await printService.printReport(formatted); }
Tambahkan fungsi baru. Saya kadang-kadang menyebut mereka "fungsi skrip" karena mereka tidak membawa implementasi, mereka menentukan urutan panggilan bagian terurai tanggung jawab kami. Jelas, dua baris pertama, dua tanggung jawab terdekomposisi pertama bertepatan dengan dua baris pertama dari fungsi yang diterapkan sebelumnya. Sama seperti dua langkah pertama dari dua tugas yang dijelaskan oleh seorang analis bisnis bersamaan.
Jadi, untuk menambahkan fungsionalitas baru ke proyek, kami menambahkan metode skrip baru dan layanan printService baru. File lama tidak diubah. Artinya, metode penulisan kode ini baik dari sudut pandang dua prinsip. Dan SRP dan Buka-Tutup
Alternatif
Saya juga ingin menyebutkan alternatif, cara bersaing untuk mendapatkan kode terurai dengan baik yang terlihat seperti ini - pertama kita menulis kode "di dahi", kemudian refactor menggunakan berbagai teknik, misalnya, menurut buku Fowler "Refactoring". Metode-metode ini mengingatkan saya pada pendekatan matematika untuk permainan catur, di mana Anda tidak mengerti apa yang sebenarnya Anda lakukan dalam hal strategi, Anda hanya menghitung "berat" dari posisi Anda dan mencoba untuk memaksimalkannya dengan membuat gerakan. Saya tidak suka pendekatan ini karena satu alasan kecil - untuk menyebutkan metode dan variabel sudah sulit, dan ketika mereka tidak memiliki nilai bisnis, itu menjadi tidak mungkin. Misalnya, jika teknik ini menyarankan Anda perlu memilih 6 garis yang identik dari sini dan dari sana, lalu menyorotinya, apa yang Anda sebut metode ini? someSixIdenticalLines ()?
Saya ingin melakukan reservasi - Saya tidak berpikir metode ini buruk, saya hanya tidak bisa belajar bagaimana menggunakannya.
Total
Dalam mengikuti prinsip, Anda dapat menemukan manfaat.
Definisi "harus ada satu tanggung jawab" tidak berfungsi.
Ada definisi yang lebih baik dan sejumlah fitur tidak langsung, yang disebut kode berbau menandakan kebutuhan untuk membusuk.
Pendekatan "membagi dan menaklukkan" memungkinkan Anda untuk segera menulis kode terstruktur dengan baik dan mendokumentasikan diri.