Prinsip Tanggung Jawab Tunggal. Tidak sesederhana kedengarannya

gambar Prinsip tanggung jawab tunggal, ia adalah prinsip tanggung jawab tunggal,
dia adalah prinsip variabilitas seragam - seorang pria yang sangat licin untuk memahami dan pertanyaan gugup seperti itu pada wawancara seorang programmer.


Kenalan serius pertama dengan prinsip ini terjadi pada saya di awal tahun pertama, ketika yang muda dan hijau dibawa ke hutan untuk mendapatkan murid nyata dari larva.


Di hutan, kami dibagi menjadi kelompok yang masing-masing terdiri dari 8-9 orang dan mengadakan kompetisi - kelompok mana yang akan minum sebotol vodka lebih cepat, asalkan orang pertama dalam kelompok menuangkan vodka ke dalam gelas, minuman kedua, dan yang ketiga menggigit. Setelah menyelesaikan operasinya, unit berdiri di akhir antrian grup.


Kasus ketika ukuran antrian adalah kelipatan dari tiga, dan merupakan implementasi SRP yang baik.


Definisi 1. Tanggung jawab tunggal.


Definisi resmi dari prinsip tanggung jawab tunggal (SRP) menunjukkan bahwa setiap objek memiliki tanggung jawab dan alasan keberadaannya sendiri, dan tanggung jawab ini hanya memiliki satu.


Pertimbangkan objek Tippler.
Untuk memenuhi prinsip SRP, kami membagi tanggung jawab menjadi tiga:


  • Satu tuang ( PourOperation )
  • One Drinks ( DrinkUpOperation )
  • One snack ( TakeBiteOperation )

Setiap peserta dalam proses bertanggung jawab atas satu komponen proses, yaitu, ia memiliki satu tanggung jawab atom - untuk minum, menuangkan atau menggigit.


Minuman keras, pada gilirannya, adalah penglihatan untuk operasi ini:


lass Tippler { //... void Act(){ _pourOperation.Do() //  _drinkUpOperation.Do() //  _takeBiteOperation.Do() //  } } 

gambar

Mengapa


Pemrogram manusia menulis kode untuk pria monyet, dan pria monyet itu lalai, bodoh dan selalu terburu-buru di suatu tempat. Dia dapat memegang dan memahami sekitar 3 sampai 7 istilah sekaligus.
Dalam hal minuman keras, ketentuan ini adalah tiga. Namun, jika kita menulis kodenya dengan satu lembar, maka tangan, kacamata, pembantaian, dan perdebatan tanpa akhir tentang politik akan muncul di dalamnya. Dan semua ini akan ada di tubuh satu metode. Saya yakin Anda telah melihat kode seperti itu dalam latihan Anda. Bukan tes yang paling manusiawi untuk jiwa.


Di sisi lain, pria monyet dipenjara karena memodelkan objek dunia nyata di kepalanya. Dalam imajinasinya, ia dapat mendorong mereka bersama-sama, mengumpulkan benda-benda baru dari mereka dan membongkar mereka dengan cara yang sama. Bayangkan sebuah model mobil tua. Anda dapat membuka pintu dalam imajinasi Anda, membuka trim pintu dan melihat mekanisme pengangkat jendela di sana, di dalamnya akan ada roda gigi. Tetapi Anda tidak dapat melihat semua komponen mesin secara bersamaan, dalam satu "daftar". Setidaknya "manusia monyet" tidak bisa.


Oleh karena itu, pemrogram manusia menguraikan mekanisme kompleks menjadi satu set elemen yang kurang kompleks dan bekerja. Namun, penguraian dapat dilakukan dengan cara yang berbeda: pada banyak mobil tua - saluran keluar pintu, dan pada yang modern - kegagalan kunci elektronik mencegah mesin dari start, yang menghasilkan selama perbaikan.


Jadi, SRP adalah prinsip yang menjelaskan BAGAIMANA membusuk, yaitu tempat menggambar garis pemisah .


Dia mengatakan bahwa dekomposisi harus didasarkan pada prinsip pemisahan "tanggung jawab", yaitu, sesuai dengan tugas berbagai objek.


gambar

Mari kita kembali ke minuman keras dan keuntungan yang didapat monyet saat membusuk:


  • Kode ini menjadi sangat jelas di setiap level.
  • Beberapa programmer dapat menulis kode sekaligus (masing-masing menulis elemen terpisah)
  • Pengujian otomatis disederhanakan - elemen yang lebih sederhana, semakin mudah untuk diuji
  • Dari ketiga operasi ini, di masa depan, Anda dapat menambahkan pelahap (hanya menggunakan TakeBitOperation ), the Alcoholic (hanya menggunakan DrinkUpOperation langsung dari botol) dan memenuhi banyak persyaratan bisnis lainnya.

Dan, tentu saja, kontra:


  • Harus membuat lebih banyak jenis.
  • Seorang peminum akan minum untuk pertama kalinya beberapa jam lebih lambat dari yang dia bisa

Definisi 2. Variabilitas terpadu.


Bolehkan tuan-tuan! Kelas minum juga memenuhi satu tanggung jawab - minum! Dan secara umum, kata "tanggung jawab" adalah konsep yang sangat kabur. Seseorang bertanggung jawab atas nasib umat manusia, dan seseorang bertanggung jawab untuk mengangkat penguin yang terbalik di tiang.


Pertimbangkan dua implementasi bingo. Yang pertama, yang disebutkan di atas, berisi tiga kelas - menuangkan, minum dan menggigit.


Yang kedua ditulis melalui metodologi Forward and Only Forward dan berisi semua logika dalam metode Act :


 //      .    lass BrutTippler { //... void Act(){ //  if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity)) throw new OverdrunkException(); //  if(!_hand.TryDrink(from: _glass, size: _glass.Capacity)) throw new OverdrunkException(); // for(int i = 0; i< 3; i++){ var food = _foodStore.TakeOrDefault(); if(food==null) throw new FoodIsOverException(); _hand.TryEat(food); } } } 

Kedua kelas ini, dari sudut pandang pengamat luar, terlihat persis sama dan memenuhi tanggung jawab tunggal "minum".


Malu!


Kemudian kami menjelajahi Internet dan mencari tahu definisi lain SRP - prinsip variabilitas seragam.


Definisi ini menyatakan bahwa “ Modul memiliki satu dan hanya satu alasan untuk perubahan .” Yaitu, "Tanggung jawab adalah kesempatan untuk perubahan."


Sekarang semuanya jatuh ke tempatnya. Secara terpisah, Anda dapat mengubah prosedur menuangkan, minum dan menggigit, dan dalam minuman keras itu sendiri kita hanya dapat mengubah urutan dan komposisi operasi, misalnya, memindahkan camilan sebelum minum atau menambahkan bacaan roti panggang.


Dalam pendekatan Penerusan dan Penerusan Hanya, semua yang dapat diubah diubah hanya dalam metode Tindakan . Ini dapat dibaca dan efektif dalam kasus ketika ada sedikit logika dan jarang berubah, tetapi sering berakhir dengan metode yang mengerikan masing-masing 500 baris, dengan jumlah lebih banyak jika diperlukan untuk masuknya Rusia ke NATO.


Definisi 3. Lokalisasi perubahan.


Peminum sering tidak mengerti mengapa mereka bangun di apartemen orang lain, atau di mana ponsel mereka. Saatnya untuk menambahkan pencatatan yang terperinci.


Mari mulai mencatat dengan proses penuangan:


 class PourOperation: IOperation{ PourOperation(ILogger log /*....*/){/*...*/} //... void Do(){ _log.Log($"Before pour with {_hand} and {_bottle}"); //Pour business logic ... _log.Log($"After pour with {_hand} and {_bottle}"); } } 

Dengan merangkumnya dalam PourOperation , kami bertindak bijak dalam hal tanggung jawab dan enkapsulasi, tetapi sekarang dengan prinsip variabilitas, kami sekarang merasa malu. Selain operasi itu sendiri, yang dapat berubah, logging itu sendiri menjadi variabel. Kami harus memisahkan dan membuat logger khusus untuk operasi penuangan:


 interface IPourLogger{ void LogBefore(IHand, IBottle){} void LogAfter(IHand, IBottle){} void OnError(IHand, IBottle, Exception){} } class PourOperation: IOperation{ PourOperation(IPourLogger log /*....*/){/*...*/} //... void Do(){ _log.LogBefore(_hand, _bottle); try{ //... business logic _log.LogAfter(_hand, _bottle"); } catch(exception e){ _log.OnError(_hand, _bottle, e) } } } 

Pembaca yang teliti akan melihat bahwa LogAfter , LogBefore, dan OnError juga dapat diubah secara individual, dan, dengan analogi dengan langkah-langkah sebelumnya, akan membuat tiga kelas: PourLoggerSebelum , PourLoggerAfter, dan PourErrorLogger .


Dan mengingat bahwa ada tiga operasi untuk pesta makan - kami mendapatkan sembilan kelas logging. Hasilnya, seluruh minuman keras terdiri dari 14 kelas (!!!).


Hiperbola? Sulit! Seorang lelaki monyet dengan granat penguraian akan menghancurkan "penuang" ke dalam botol, gelas, operator penuangan, layanan pasokan air, model fisik tabrakan molekul, dan kuartal berikutnya akan mencoba mengurai dependensi tanpa variabel global. Dan percayalah - dia tidak akan berhenti.


Pada titik inilah banyak yang sampai pada kesimpulan bahwa SRP adalah dongeng dari kerajaan merah muda, dan pergi untuk memutar mie ...


... tidak pernah tahu tentang keberadaan definisi ketiga Srp:


" Hal-hal yang mirip dengan perubahan harus disimpan di satu tempat ." atau " Apa yang berubah bersama harus disimpan di satu tempat "


Yaitu, jika kita mengubah operasi logging, maka kita harus mengubahnya di satu tempat.


Ini adalah poin yang sangat penting - karena semua penjelasan SRP di atas mengatakan bahwa tipe harus dibagi ketika mereka dibagi, yaitu, menerapkan "batas atas" pada ukuran objek, dan sekarang kita berbicara tentang "batas bawah" . Dengan kata lain, SRP tidak hanya membutuhkan "menghancurkan sambil menghancurkan", tetapi juga tidak berlebihan - "jangan hancurkan hal-hal terkait . " Jangan menyulitkan yang tidak perlu. Ini adalah pertempuran hebat pisau cukur Occam dengan si monyet!


gambar

Sekarang minuman keras seharusnya lebih mudah. Selain tidak memecah logger IPourLogger menjadi tiga kelas, kami juga dapat menggabungkan semua logger menjadi satu jenis:


 class OperationLogger{ public OperationLogger(string operationName){/*..*/} public void LogBefore(object[] args){/*...*/} public void LogAfter(object[] args){/*..*/} public void LogError(object[] args, exception e){/*..*/} } 

Dan jika jenis operasi keempat ditambahkan kepada kami, maka logging siap untuk itu. Dan kode operasi itu sendiri bersih dan bebas dari kebisingan infrastruktur.


Akibatnya, kami memiliki 5 kelas untuk menyelesaikan masalah minum:


  • Menuangkan operasi
  • Operasi minuman
  • Operasi macet
  • Logger
  • Fasad Booler

Masing-masing dari mereka bertanggung jawab secara ketat untuk satu fungsi, memiliki satu alasan untuk perubahan. Semua aturan yang mirip dengan perubahan ada di dekatnya.


Contoh kehidupan nyata


Serialisasi dan Deserialisasi

Sebagai bagian dari pengembangan protokol transfer data, perlu untuk membuat serial dan deserialize beberapa jenis "Pengguna" ke dalam string.


 User{ String Name; Int Age; } 

Anda mungkin berpikir bahwa serialisasi dan deserialisasi perlu dilakukan di kelas yang berbeda:


 UserDeserializer{ String deserialize(User){...} } UserSerializer{ User serialize(String){...} } 

Karena masing-masing dari mereka memiliki tanggung jawab sendiri dan satu alasan untuk perubahan.


Tetapi mereka memiliki alasan umum untuk perubahan - "mengubah format serialisasi data."
Dan ketika mengubah format ini, serialisasi dan deserialisasi akan selalu berubah.


Menurut prinsip pelokalan perubahan, kita harus menggabungkannya menjadi satu kelas:


 UserSerializer{ String deserialize(User){...} User serialize(String){...} } 

Ini menyelamatkan kita dari kerumitan yang tidak perlu, dan kebutuhan untuk mengingat bahwa setiap kali Anda mengubah serializer, Anda perlu mengingat tentang deserializer.


Hitung dan simpan

Anda perlu menghitung pendapatan tahunan perusahaan dan menyimpannya dalam file C: \ results.txt.


Kami dengan cepat menyelesaikan ini dengan satu metode:


 void SaveGain(Company company){ //     //   } 

Sudah dari definisi tugas, jelas bahwa ada dua subtugas - "Hitung pendapatan" dan "Simpan pendapatan". Masing-masing dari mereka memiliki satu alasan untuk perubahan - "perubahan dalam metodologi perhitungan" dan "perubahan dalam format penyimpanan". Perubahan ini tidak tumpang tindih. Selain itu, kami tidak dapat menjawab satu suku kata untuk pertanyaan - “apa yang dilakukan metode SaveGain?”. Metode DAN ini menghitung pendapatan DAN menyimpan hasilnya.


Karena itu, Anda perlu membagi metode ini menjadi dua:


 Gain CalcGain(Company company){..} void SaveGain(Gain gain){..} 

Pro:


  • dapat diuji secara terpisah CalcGain
  • lebih mudah untuk melokalkan bug dan membuat perubahan
  • pembacaan kode meningkat
  • risiko kesalahan dalam setiap metode berkurang karena penyederhanaannya

Logika bisnis yang canggih

Pernah kami menulis layanan untuk registrasi otomatis klien b2b. Dan ada metode GOD dengan 200 baris konten serupa:


  • Buka 1C dan dapatkan akun
  • Dengan akun ini, buka modul pembayaran dan dapatkan di sana
  • Periksa apakah akun dengan akun seperti itu belum dibuat di server utama
  • Buat akun baru
  • Hasil pendaftaran dalam modul pembayaran dan nomor 1c ditambahkan ke layanan hasil pendaftaran
  • Tambahkan informasi akun ke tabel ini
  • Buat nomor poin untuk pelanggan ini dalam layanan poin. Berikan nomor akun layanan ini 1s.

Ada sekitar 10 operasi bisnis lagi dengan keterhubungan yang mengerikan di daftar ini. Objek akun dibutuhkan oleh hampir semua orang. ID titik dan nama klien diperlukan dalam setengah panggilan.


Setelah satu jam refactoring, kami dapat memisahkan kode infrastruktur dan beberapa nuansa bekerja dengan akun menjadi metode / kelas yang terpisah. Metode Dewa menjadi lebih mudah, tetapi ada 100 baris kode yang tersisa yang tidak ingin diurai.


Hanya beberapa hari kemudian muncul pemahaman bahwa esensi dari metode "lega" ini adalah algoritma bisnis. Dan bahwa deskripsi awal TK agak rumit. Dan ini merupakan upaya untuk memecah metode ini menjadi potongan-potongan yang akan menjadi pelanggaran SRP, dan bukan sebaliknya.


Formalisme.


Sudah waktunya untuk meninggalkan minuman keras kita sendirian. Seka air mata - kita pasti akan kembali ke sana. Sekarang kami meresmikan pengetahuan dari artikel ini.


Formalisme 1. Definisi SRP


  1. Pisahkan elemen sehingga masing-masing bertanggung jawab untuk satu hal.
  2. Tanggung jawab berarti “penyebab perubahan”. Artinya, setiap elemen hanya memiliki satu alasan untuk perubahan, dalam hal logika bisnis.
  3. Potensi perubahan logika bisnis. harus dilokalisasi. Item yang bisa berubah bersama harus berada di dekat.

Formalisme 2. Kriteria yang diperlukan untuk pemeriksaan diri.


Saya belum memenuhi kriteria yang cukup untuk implementasi SRP. Tetapi ada kondisi yang diperlukan:


1) Ajukan pertanyaan pada diri sendiri - apa yang dilakukan kelas / metode / modul / layanan ini. Anda harus menjawabnya dengan definisi sederhana. (terima kasih kepada Brightori )


penjelasan

Namun, terkadang sangat sulit untuk menemukan definisi yang sederhana


2) Memperbaiki bug atau menambahkan fitur baru memengaruhi jumlah minimum file / kelas. Idealnya, satu.


penjelasan

Karena tanggung jawab (untuk fitur atau bug) dienkapsulasi dalam satu file / kelas, maka Anda tahu persis ke mana harus mencari dan apa yang harus diedit. Sebagai contoh: fitur untuk mengubah keluaran dari operasi logging akan memerlukan hanya mengubah logger. Berlari di sekitar sisa kode tidak diperlukan.


Contoh lain adalah penambahan kontrol UI baru yang mirip dengan yang sebelumnya. Jika ini memaksa Anda untuk menambahkan 10 entitas yang berbeda dan 15 konverter yang berbeda - tampaknya Anda telah "rusak".


3) Jika beberapa pengembang bekerja pada fitur berbeda dari proyek Anda, maka kemungkinan konflik penggabungan, yaitu, probabilitas bahwa beberapa pengembang akan mengubah file / kelas yang sama pada saat yang sama, minimal.


penjelasan

Jika ketika menambahkan operasi baru "Tuang vodka di bawah meja" Anda harus menyentuh logger, operasi minum dan menuangkan - maka sepertinya tanggung jawab dibagi miring. Tentu saja, ini tidak selalu memungkinkan, tetapi Anda perlu mencoba mengurangi angka ini.


4) Ketika mengklarifikasi pertanyaan tentang logika bisnis (dari pengembang atau manajer), Anda naik secara ketat ke satu kelas / file dan menerima informasi hanya dari sana.


penjelasan

Fitur, aturan, atau algoritme ditulis dengan kompak masing-masing di satu tempat, dan tidak disebarkan oleh bendera di seluruh ruang kode.


5) Penamaan jelas.


penjelasan

Kelas atau metode kita bertanggung jawab atas satu hal, dan tanggung jawab itu tercermin dalam namanya.


AllManagersManagerService - kemungkinan besar, kelas dewa
Pembayaran Lokal - mungkin tidak


Formalisme 3. Metode pengembangan Occam-first.


Pada awal desain, manusia monyet tidak tahu dan tidak merasakan semua seluk-beluk masalah yang sedang dipecahkan dan dapat memberikan kesalahan. Anda dapat membuat kesalahan dengan berbagai cara:


  • Buat benda terlalu besar dengan menempelkan berbagai tanggung jawab
  • Berpisah, membagi satu tanggung jawab menjadi beberapa jenis
  • Batas tanggung jawab yang didefinisikan secara tidak benar

Penting untuk mengingat aturan: "lebih baik membuat kesalahan besar," atau "tidak yakin - jangan berpisah." Jika, misalnya, kelas Anda mengumpulkan dua tanggung jawab, maka masih dapat dimengerti dan dapat dibagi menjadi dua dengan sedikit perubahan dalam kode klien. Mengumpulkan gelas dari pecahan kaca biasanya lebih sulit karena konteksnya tersebar di beberapa file dan kurangnya ketergantungan yang diperlukan dalam kode klien.


Saatnya untuk mengakhiri


Ruang lingkup SRP tidak terbatas pada OOP dan SOLID. Ini berlaku untuk metode, fungsi, kelas, modul, layanan mikro dan layanan. Ini berlaku untuk pengembangan "figax-figax-dan-in-prod" dan "rocket-sainz", membuat dunia sedikit lebih baik di mana-mana. Jika Anda memikirkannya, ini hampir merupakan prinsip dasar dari semua teknik. Teknik mesin, sistem kontrol, dan memang semua sistem yang kompleks dibangun dari komponen, dan "fragmentasi tidak lengkap" membuat desainer tidak fleksibel, "fragmentasi" - efisiensi, dan batas-batas yang salah - alasan dan ketenangan pikiran.


gambar

SRP tidak ditemukan secara alami dan bukan bagian dari ilmu pasti. Ini merangkak keluar dari keterbatasan biologis dan psikologis kita.Ini hanya cara untuk mengendalikan dan mengembangkan sistem yang kompleks menggunakan otak monyet manusia. Dia memberi tahu kita cara menguraikan sistem. Kata-kata aslinya membutuhkan cukup banyak telepati, tapi saya harap artikel ini sedikit menghilangkan tabir asap.

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


All Articles