Saya berusaha mematuhi Prinsip Tanggung Jawab Tunggal (SRP) sebanyak mungkin dan terbiasa dengan pola tertentu (untuk SRP tentang metode) sangat bergantung pada delegasi. Saya ingin tahu apakah pendekatan ini masuk akal atau ada masalah parah dengan itu.
Misalnya, untuk memeriksa input ke konstruktor, saya bisa memperkenalkan metode berikut ( Stream
inputnya acak, bisa apa saja)
private void CheckInput(Stream stream)
{
if(stream == null)
{
throw new ArgumentNullException();
}
if(!stream.CanWrite)
{
throw new ArgumentException();
}
}
Metode ini (bisa dibilang) melakukan lebih dari satu hal
- Periksa input
- Lempar pengecualian yang berbeda
Untuk mematuhi SRP saya mengubah logika menjadi
private void CheckInput(Stream stream,
params (Predicate<Stream> predicate, Action action)[] inputCheckers)
{
foreach(var inputChecker in inputCheckers)
{
if(inputChecker.predicate(stream))
{
inputChecker.action();
}
}
}
Yang seharusnya hanya melakukan satu hal (bukan?): Periksa input. Untuk pengecekan aktual input dan melemparkan pengecualian saya telah memperkenalkan metode seperti
bool StreamIsNull(Stream s)
{
return s == null;
}
bool StreamIsReadonly(Stream s)
{
return !s.CanWrite;
}
void Throw<TException>() where TException : Exception, new()
{
throw new TException();
}
dan bisa memanggil CheckInput
seperti
CheckInput(stream,
(this.StreamIsNull, this.Throw<ArgumentNullException>),
(this.StreamIsReadonly, this.Throw<ArgumentException>))
Apakah ini lebih baik daripada opsi pertama, atau apakah saya memperkenalkan kompleksitas yang tidak perlu? Apakah ada cara saya masih bisa memperbaiki pola ini, jika itu layak?
sumber
CheckInput
masih melakukan banyak hal: Ia melakukan iterasi pada array dan memanggil fungsi predikat dan memanggil fungsi aksi. Apakah itu bukan pelanggaran terhadap SRP?Jawaban:
SRP mungkin merupakan prinsip perangkat lunak yang paling disalahpahami.
Aplikasi perangkat lunak dibangun dari modul, yang dibangun dari modul, yang dibangun dari ...
Di bagian bawah, fungsi tunggal seperti
CheckInput
hanya akan berisi sedikit logika, tetapi ketika Anda naik ke atas, setiap modul berturut-turut merangkum lebih banyak logika dan ini normal .SRP bukan tentang melakukan aksi atom tunggal . Ini tentang memiliki satu tanggung jawab, bahkan jika tanggung jawab itu memerlukan beberapa tindakan ... dan pada akhirnya ini tentang pemeliharaan dan pengujian :
Fakta yang
CheckInput
diimplementasikan dengan dua pemeriksaan dan menaikkan dua pengecualian yang berbeda tidak relevan sampai batas tertentu.CheckInput
memiliki tanggung jawab yang sempit: memastikan bahwa input sesuai dengan persyaratan. Ya, ada banyak persyaratan, tetapi ini tidak berarti ada banyak tanggung jawab. Ya, Anda dapat membagi cek, tetapi bagaimana itu membantu? Pada titik tertentu cek harus didaftar dengan cara tertentu.Mari kita bandingkan:
melawan:
Sekarang,
CheckInput
tidak banyak ... tetapi peneleponnya berbuat lebih banyak!Anda telah menggeser daftar persyaratan dari
CheckInput
, di mana mereka dienkapsulasi, ke diConstructor
mana mereka terlihat.Apakah ini perubahan yang baik? Tergantung:
CheckInput
hanya disebut di sana: itu masih bisa diperdebatkan, di satu sisi itu membuat persyaratan terlihat, di sisi lain itu mengacaukan kode;CheckInput
dipanggil beberapa kali dengan persyaratan yang sama , maka itu melanggar KERING dan Anda memiliki masalah enkapsulasi.Penting untuk menyadari bahwa satu tanggung jawab dapat menyiratkan banyak pekerjaan. "Otak" mobil yang dapat mengendalikan diri memiliki satu tanggung jawab:
Ini adalah tanggung jawab tunggal, tetapi membutuhkan koordinasi satu ton sensor dan aktor, mengambil banyak keputusan, dan bahkan mungkin memiliki persyaratan yang saling bertentangan 1 ...
... Namun, itu semua dikemas. Jadi klien tidak peduli.
1 keselamatan penumpang, keselamatan orang lain, menghormati peraturan, ...
sumber
Mengutip Paman Bob tentang SRP ( https://8thlight.com/blog/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html ):
Dia menjelaskan bahwa modul perangkat lunak harus mengatasi kekhawatiran pemangku kepentingan tertentu. Karena itu, jawablah pertanyaan Anda:
IMO, Anda hanya melihat satu metode, ketika Anda harus melihat level yang lebih tinggi (level kelas dalam hal ini). Mungkin kita harus melihat apa yang sedang dilakukan kelas Anda (dan ini membutuhkan lebih banyak penjelasan tentang skenario Anda). Untuk saat ini, kelas Anda masih melakukan hal yang sama. Misalnya, jika besok ada beberapa permintaan perubahan tentang beberapa validasi (misalnya: "sekarang streaming dapat menjadi nol"), maka Anda masih perlu pergi ke kelas ini dan mengubah hal-hal di dalamnya.
sumber
checkInputs()
harus dibagi, katakan menjadicheckMarketingInputs()
dancheckRegulatoryInputs()
. Kalau tidak apa-apa, menggabungkan semuanya menjadi satu metode.Tidak, perubahan ini tidak diinformasikan oleh SRP.
Tanyakan pada diri Anda mengapa tidak ada check di checker Anda untuk "objek yang dilewati adalah aliran" . Jawabannya jelas: bahasa mencegah penelepon menyusun program yang lewat di non-stream.
Sistem tipe C # tidak cukup untuk memenuhi kebutuhan Anda; cek Anda menerapkan penegakan invarian yang tidak dapat diungkapkan dalam sistem jenis hari ini . Jika ada cara untuk mengatakan bahwa metode ini mengambil aliran yang tidak dapat ditulisi, Anda akan menulis itu, tetapi tidak ada, jadi Anda melakukan hal terbaik berikutnya: Anda menerapkan pembatasan jenis saat runtime. Semoga Anda juga mendokumentasikannya, sehingga pengembang yang menggunakan metode Anda tidak harus melanggarnya, gagal dalam kasus pengujian mereka, dan kemudian memperbaiki masalahnya.
Menempatkan tipe pada suatu metode bukanlah pelanggaran terhadap Prinsip Tanggung Jawab Tunggal; tidak ada metode yang menegakkan prasyaratnya atau menegaskan prasyarat kondisinya.
sumber
Tidak semua tanggung jawab diciptakan sama.
Ini ada dua laci. Mereka berdua memiliki satu tanggung jawab. Mereka masing-masing memiliki nama yang memberi tahu Anda apa yang termasuk di dalamnya. Salah satunya adalah laci perak. Yang lainnya adalah laci sampah.
Jadi apa bedanya? Laci perak menjelaskan apa yang bukan miliknya. Laci sampah bagaimanapun menerima apapun yang cocok. Mengambil sendok dari laci alat makan tampaknya sangat salah. Namun saya sulit sekali memikirkan apa pun yang akan terlewatkan jika dihapus dari laci sampah. Yang benar adalah Anda dapat mengklaim sesuatu memiliki tanggung jawab tunggal tetapi menurut Anda mana yang memiliki tanggung jawab tunggal yang lebih terfokus?
Objek yang memiliki satu tanggung jawab tidak berarti hanya satu hal yang dapat terjadi di sini. Tanggung jawab bisa bersarang. Tetapi tanggung jawab bersarang itu masuk akal, mereka seharusnya tidak mengejutkan Anda ketika Anda menemukannya di sini dan Anda harus merindukan mereka jika mereka pergi.
Jadi saat Anda menawarkan
CheckInput(Stream stream);
Saya tidak merasa khawatir bahwa keduanya memeriksa input dan melempar pengecualian. Saya akan khawatir jika keduanya memeriksa input dan menyimpan input juga. Itu kejutan yang tidak menyenangkan. Satu aku tidak akan ketinggalan jika itu hilang.
sumber
Ketika Anda mengikat diri Anda dalam simpul dan menulis kode aneh untuk menyesuaikan dengan Prinsip Perangkat Lunak Penting, biasanya Anda telah salah memahami prinsip tersebut (meskipun kadang-kadang prinsipnya salah). Seperti yang ditunjukkan oleh Matthieu, seluruh makna SRP tergantung pada definisi "tanggung jawab".
Pemrogram berpengalaman melihat prinsip-prinsip ini dan mengaitkannya dengan ingatan kode yang kami buat; programmer kurang berpengalaman melihat mereka dan mungkin tidak ada hubungannya sama sekali. Ini adalah abstraksi yang mengambang di ruang angkasa, semuanya tersenyum dan tidak ada kucing. Jadi mereka menebak, dan biasanya berjalan buruk. Sebelum Anda mengembangkan pemrograman akal sehat, perbedaan antara kode rumit berlebih yang aneh dan kode normal sama sekali tidak jelas.
Ini bukan perintah agama yang harus Anda patuhi terlepas dari konsekuensi pribadi. Ini lebih dari aturan praktis yang dimaksudkan untuk memformalkan satu elemen pemrograman akal kuda, dan membantu Anda menjaga kode Anda sesederhana dan sejelas mungkin. Jika memiliki efek sebaliknya, Anda benar untuk mencari beberapa masukan dari luar.
Dalam pemrograman, Anda tidak dapat melakukan kesalahan lebih banyak daripada mencoba menyimpulkan makna pengidentifikasi dari prinsip pertama hanya dengan melihatnya, dan itu berlaku untuk pengidentifikasi dalam menulis tentang pemrograman sama halnya dengan pengidentifikasi dalam kode aktual.
sumber
Peran CheckInput
Pertama, biarkan aku meletakkan jelas di luar sana,
CheckInput
adalah melakukan satu hal, bahkan jika itu adalah memeriksa berbagai aspek. Pada akhirnya memeriksa input . Orang bisa berpendapat bahwa itu bukan satu hal jika Anda berurusan dengan metode yang disebutDoSomething
, tapi saya pikir aman untuk mengasumsikan bahwa memeriksa input tidak terlalu kabur.Menambahkan pola ini untuk predikat dapat berguna jika Anda ingin tidak ingin logika untuk memeriksa input ditempatkan ke dalam kelas Anda, tetapi pola ini agak bertele-tele untuk apa yang ingin Anda capai. Mungkin jauh lebih langsung untuk hanya melewati antarmuka
IStreamValidator
dengan metode tunggalisValid(Stream)
jika itu yang ingin Anda peroleh. Setiap kelas implementasiIStreamValidator
dapat menggunakan predikat sepertiStreamIsNull
atauStreamIsReadonly
jika mereka inginkan, tetapi kembali ke titik pusat, itu adalah perubahan yang agak konyol untuk kepentingan mempertahankan prinsip tanggung jawab tunggal.Cek kewarasan
Adalah ide saya bahwa kita semua diizinkan melakukan "pemeriksaan kewarasan" untuk memastikan bahwa Anda setidaknya berurusan dengan Stream yang tidak nol dan dapat ditulisi, dan pemeriksaan dasar ini tidak membuat kelas Anda validator stream. Pikiran Anda, pemeriksaan yang lebih canggih akan lebih baik dibiarkan di luar kelas Anda, tetapi di situlah garis ditarik. Setelah Anda perlu mulai mengubah keadaan aliran Anda dengan membaca darinya atau mendedikasikan sumber daya ke arah validasi, Anda sudah mulai melakukan validasi formal atas aliran Anda dan inilah yang harus ditarik ke dalam kelasnya sendiri.
Kesimpulan
Pikiranku adalah bahwa jika Anda menerapkan suatu pola untuk mengatur lebih baik suatu aspek dari kelas Anda, itu pantas untuk berada di kelasnya sendiri. Karena suatu pola tidak cocok, Anda juga harus mempertanyakan apakah pola itu memang termasuk dalam kelasnya sendiri. Pikiran saya adalah bahwa kecuali Anda yakin validasi aliran kemungkinan akan berubah di masa mendatang, dan terutama jika Anda yakin validasi ini mungkin bersifat dinamis, maka pola yang Anda gambarkan adalah ide yang bagus, bahkan jika itu mungkin awalnya sepele. Kalau tidak, tidak perlu dengan sewenang-wenang membuat program Anda lebih kompleks. Mari kita sebut sekop sekop. Validasi adalah satu hal, tetapi memeriksa input nol bukan validasi, dan oleh karena itu saya pikir Anda bisa aman menyimpannya di kelas Anda tanpa melanggar prinsip tanggung jawab tunggal.
sumber
Prinsipnya dengan tegas tidak menyatakan sepotong kode harus "hanya melakukan satu hal".
"Tanggung jawab" dalam SRP harus dipahami pada tingkat persyaratan. Tanggung jawab kode adalah untuk memenuhi persyaratan bisnis. SRP dilanggar jika suatu objek memenuhi lebih dari satu persyaratan bisnis independen . Dengan mandiri itu berarti bahwa satu persyaratan dapat berubah sementara persyaratan lainnya tetap ada.
Bisa dibayangkan bahwa persyaratan bisnis baru diperkenalkan yang berarti objek khusus ini tidak harus memeriksa dapat dibaca, sementara persyaratan bisnis lain masih memerlukan objek untuk memeriksa dapat dibaca? Tidak, karena persyaratan bisnis tidak menentukan detail implementasi di tingkat itu.
Contoh aktual pelanggaran SRP adalah kode seperti ini:
Kode ini sangat sederhana, tetapi masih dapat dibayangkan bahwa teks akan berubah secara independen dari tanggal pengiriman yang diharapkan, karena ini diputuskan oleh berbagai bagian bisnis.
sumber
Saya suka intinya dari jawaban @ EricLippert :
EricLippert benar bahwa ini adalah masalah untuk sistem tipe. Dan karena Anda ingin menggunakan prinsip tanggung jawab tunggal (SRP), maka Anda pada dasarnya membutuhkan sistem tipe untuk bertanggung jawab atas pekerjaan ini.
Sebenarnya mungkin untuk melakukan ini di C #. Kita dapat menangkap literal
null
pada waktu kompilasi, kemudian menangkap non-literalnull
pada saat run-time. Itu tidak sebagus pemeriksaan waktu kompilasi penuh, tetapi ini merupakan peningkatan yang ketat agar tidak tertangkap pada waktu kompilasi.Jadi, Anda tahu bagaimana C #
Nullable<T>
? Mari kita balikkan itu dan buatNonNullable<T>
:Sekarang, alih-alih menulis
, cukup tulis:
Lalu, ada tiga use case:
Panggilan pengguna
Foo()
dengan non-nullStream
:Ini adalah use case yang diinginkan, dan ini berfungsi dengan atau tanpa
NonNullable<>
.Panggilan pengguna
Foo()
dengan nolStream
:Ini adalah kesalahan panggilan. Di sini
NonNullable<>
membantu memberi tahu pengguna bahwa mereka seharusnya tidak melakukan ini, tetapi itu tidak benar-benar menghentikan mereka. Either way, ini menghasilkan run-timeNullArgumentException
.Panggilan pengguna
Foo()
dengannull
:null
tidak akan secara implisit dikonversi menjadiNonNullable<>
, sehingga pengguna mendapat kesalahan dalam IDE, sebelum waktu-berjalan. Ini mendelegasikan pemeriksaan-nol ke sistem tipe, seperti yang disarankan oleh SRP.Anda dapat memperluas metode ini untuk menegaskan hal-hal lain tentang argumen Anda juga. Misalnya, karena Anda ingin aliran yang dapat ditulisi, Anda dapat menentukan suatu
struct WriteableStream<T> where T:Stream
yang memeriksa keduanyanull
danstream.CanWrite
dalam konstruktor. Ini masih akan menjadi pemeriksaan jenis run-time, tetapi:Itu menghiasi jenis dengan
WriteableStream
kualifikasi, menandakan perlunya penelepon.Itu melakukan pemeriksaan di satu tempat dalam kode, sehingga Anda tidak perlu mengulangi pemeriksaan dan
throw InvalidArgumentException
setiap kali.Ini lebih sesuai dengan SRP dengan mendorong tugas-tugas pengecekan tipe ke sistem tipe (seperti yang diperpanjang oleh dekorator generik).
sumber
Pendekatan Anda saat ini bersifat prosedural. Anda memecah
Stream
objek dan memvalidasinya dari luar. Jangan lakukan itu - itu merusak enkapsulasi. Biarkan yangStream
bertanggung jawab atas validasinya sendiri. Kami tidak dapat mencoba menerapkan SRP sampai kami memiliki beberapa kelas untuk menerapkannya.Inilah
Stream
yang melakukan tindakan hanya jika melewati validasi:Tapi sekarang kita melanggar SRP! "Kelas seharusnya hanya punya satu alasan untuk berubah." Kami mendapat campuran 1) validasi dan 2) logika aktual. Kami punya dua alasan yang mungkin perlu diubah.
Kita bisa menyelesaikan ini dengan memvalidasi dekorator . Pertama, kita perlu mengubah
Stream
antarmuka kita menjadi dan mengimplementasikannya sebagai kelas konkret.Kita sekarang dapat menulis dekorator yang membungkus a
Stream
, melakukan validasi dan menolak diberikanStream
untuk logika tindakan yang sebenarnya.Kita sekarang dapat menyusun ini dengan cara apa pun yang kita suka:
Ingin validasi tambahan? Tambahkan dekorator lain.
sumber
Pekerjaan kelas adalah menyediakan layanan yang memenuhi kontrak . Kelas selalu memiliki kontrak: seperangkat persyaratan untuk menggunakannya, dan berjanji akan membuat tentang negara dan output asalkan persyaratan terpenuhi. Kontrak ini mungkin eksplisit, melalui dokumentasi dan / atau pernyataan, atau implisit, tetapi selalu ada.
Bagian dari kontrak kelas Anda adalah bahwa penelepon memberikan argumen kepada konstruktor yang tidak boleh nol. Menerapkan kontrak adalah tanggung jawab kelas, jadi untuk memeriksa bahwa penelepon telah memenuhi bagian kontraknya, dapat dengan mudah dianggap berada dalam lingkup tanggung jawab kelas.
Gagasan bahwa suatu kelas mengimplementasikan kontrak adalah karena Bertrand Meyer , perancang bahasa pemrograman Eiffel dan gagasan desain berdasarkan kontrak . Bahasa Eiffel membuat spesifikasi dan memeriksa bagian kontrak bahasa.
sumber
Seperti yang telah ditunjukkan dalam jawaban lain, SRP sering disalahpahami. Ini bukan tentang memiliki kode atom yang hanya melakukan satu fungsi. Ini tentang memastikan bahwa objek dan metode Anda hanya melakukan satu hal, dan bahwa satu hal hanya dilakukan di satu tempat.
Mari kita lihat contoh yang buruk dalam kode pseudo.
Dalam contoh kami yang agak absurd, "tanggung jawab" konstruktor Math # adalah membuat objek matematika dapat digunakan. Ia melakukannya dengan terlebih dahulu membersihkan input, kemudian dengan memastikan nilainya tidak -1.
Ini adalah SRP yang valid karena konstruktor hanya melakukan satu hal. Ini mempersiapkan objek matematika. Namun itu tidak terlalu terpelihara. Itu melanggar KERING.
Jadi mari kita lewati lagi
Dalam lulus ini kami mendapat sedikit lebih baik tentang KERING, tetapi kami masih memiliki cara untuk pergi dengan KERING. SRP di sisi lain sepertinya agak mati. Kami sekarang memiliki dua fungsi dengan pekerjaan yang sama. CleanX dan cleanY membersihkan input.
Mari kita coba lagi
Sekarang akhirnya lebih baik tentang KERING, dan SRP tampaknya setuju. Kami hanya memiliki satu tempat yang melakukan pekerjaan "membersihkan".
Secara teori kode ini lebih mudah dikelola dan lebih baik ketika kita memperbaiki bug dan memperketat kodenya, kita hanya perlu melakukannya di satu tempat.
Dalam kebanyakan kasus dunia nyata objek akan lebih kompleks dan SRP akan diterapkan di banyak objek. Misalnya usia mungkin milik Ayah, Ibu, Putra, Putri, jadi alih-alih memiliki 4 kelas yang mengetahui usia sejak tanggal lahir Anda memiliki kelas Person yang melakukan itu dan 4 kelas mewarisi dari itu. Tapi saya harap contoh ini membantu menjelaskan. SRP bukan tentang aksi atom, tetapi tentang "pekerjaan" yang dilakukan.
sumber
Berbicara tentang SRP, Paman Bob tidak suka cek nol ditaburkan di mana-mana. Secara umum Anda, sebagai tim, harus menghindari menggunakan parameter nol untuk konstruktor bila memungkinkan. Ketika Anda mempublikasikan kode Anda di luar tim Anda, hal-hal dapat berubah.
Menegakkan non-nullability parameter konstruktor tanpa terlebih dahulu memastikan keterpaduan kelas yang bersangkutan menghasilkan mengasapi kode panggilan, terutama tes.
Jika Anda benar-benar ingin menegakkan kontrak seperti itu pertimbangkan untuk menggunakan
Debug.Assert
atau sesuatu yang serupa untuk mengurangi kekacauan:sumber