Klarifikasi Prinsip Tanggung Jawab Tunggal

64

Prinsip Tanggung Jawab Tunggal menyatakan bahwa kelas harus melakukan satu dan hanya satu hal. Beberapa kasus dipotong cukup jelas. Namun, yang lain sulit karena apa yang tampak seperti "satu hal" ketika dilihat pada tingkat abstraksi tertentu mungkin banyak hal bila dilihat pada tingkat yang lebih rendah. Saya juga takut bahwa jika Prinsip Tanggung Jawab Tunggal dihormati pada tingkat yang lebih rendah, kode ravioli verbose yang dipisahkan secara berlebihan, di mana lebih banyak baris dihabiskan untuk menciptakan kelas-kelas kecil untuk segala sesuatu dan menyalurkan informasi di sekitar daripada benar-benar menyelesaikan masalah yang dihadapi, dapat dihasilkan.

Bagaimana Anda menggambarkan apa arti "satu hal"? Apa saja tanda-tanda konkret bahwa kelas benar-benar melakukan lebih dari "satu hal"?

dsimcha
sumber
6
+1 untuk "kode ravioli" teratas. Di awal karir saya, saya adalah salah satu dari orang-orang yang mengambilnya terlalu jauh. Tidak hanya dengan kelas, tetapi dengan modularisasi metode juga. Kode saya dibumbui dengan banyak metode kecil yang melakukan sesuatu yang sederhana, hanya demi memecah masalah menjadi potongan-potongan kecil yang bisa muat di layar tanpa menggulir. Jelas, ini sering terlalu jauh.
Bobby Tables

Jawaban:

50

Saya sangat suka cara Robert C. Martin (Paman Bob) menyatakan kembali Prinsip Tanggung Jawab Tunggal (terkait dengan PDF) :

Seharusnya tidak ada lebih dari satu alasan bagi kelas untuk berubah

Ini agak berbeda dari definisi tradisional "hanya melakukan satu hal", dan saya suka ini karena memaksa Anda untuk mengubah cara Anda berpikir tentang kelas Anda. Alih-alih memikirkan "apakah ini melakukan satu hal?", Anda malah berpikir tentang apa yang bisa berubah dan bagaimana perubahan itu memengaruhi kelas Anda. Jadi misalnya, jika perubahan basis data apakah kelas Anda perlu berubah? Bagaimana jika perangkat keluaran berubah (misalnya layar, atau perangkat seluler, atau printer)? Jika kelas Anda perlu diubah karena perubahan dari banyak arah lain, maka itu pertanda bahwa kelas Anda memiliki terlalu banyak tanggung jawab.

Dalam artikel terkait, Paman Bob menyimpulkan:

SRP adalah salah satu prinsip yang paling sederhana, dan salah satu yang paling sulit untuk diperbaiki. Tanggung jawab bersama adalah sesuatu yang kita lakukan secara alami. Menemukan dan memisahkan tanggung jawab tersebut satu sama lain adalah sebagian besar dari desain perangkat lunak sebenarnya.

Paddyslacker
sumber
2
Saya suka cara dia menyatakannya juga, sepertinya lebih mudah untuk memeriksa ketika terkait dengan perubahan daripada "tanggung jawab" abstrak.
Matthieu M.
Itu sebenarnya cara yang bagus untuk menjelaskannya. Saya suka itu. Sebagai catatan, saya biasanya cenderung menganggap SRP sebagai penerapan metode yang jauh lebih kuat. Terkadang sebuah kelas hanya perlu melakukan dua hal (mungkin kelasnya yang menjembatani dua domain), tetapi sebuah metode seharusnya hampir tidak pernah melakukan lebih dari apa yang dapat Anda jelaskan secara ringkas dengan tanda tangan tipenya.
CodexArcanum
1
Hanya menunjukkan ini kepada Grad saya - bacaan yang bagus dan pengingat yang bagus untuk diriku sendiri.
Martijn Verburg
1
Oke, ini masuk akal ketika Anda menggabungkannya dengan gagasan bahwa kode yang tidak direkayasa ulang hanya akan merencanakan perubahan yang mungkin terjadi di masa mendatang, bukan untuk setiap kemungkinan perubahan. Saya akan menyatakan kembali ini, kemudian, sedikit karena "seharusnya hanya ada satu alasan bagi kelas untuk berubah yang mungkin terjadi di masa mendatang". Ini mendorong kesederhanaan yang mendukung bagian-bagian desain yang tidak mungkin berubah dan memisahkan bagian-bagian yang cenderung berubah.
dsimcha
18

Saya terus bertanya pada diri sendiri masalah apa yang SRP coba selesaikan? Kapan SRP membantu saya? Inilah yang saya pikirkan:

Anda harus memperbaiki tanggung jawab / fungsionalitas di luar kelas ketika:

1) Anda telah menduplikasi fungsi (KERING)

2) Anda menemukan bahwa kode Anda memerlukan abstraksi tingkat lain untuk membantu Anda memahaminya (KISS)

3) Anda menemukan bahwa bagian-bagian fungsionalitas dipahami oleh para ahli domain Anda sebagai bagian dari komponen yang berbeda (Bahasa Di mana-mana)

Anda TIDAK HARUS menolak tanggung jawab di luar kelas ketika:

1) Tidak ada fungsi duplikat.

2) Fungsionalitas tidak masuk akal di luar konteks kelas Anda. Dengan kata lain, kelas Anda menyediakan konteks di mana lebih mudah untuk memahami fungsionalitasnya.

3) Pakar domain Anda tidak memiliki konsep tanggung jawab itu.

Terpikir oleh saya bahwa jika SRP diterapkan secara luas, kami menukar satu jenis kompleksitas (mencoba membuat head atau tail of class dengan terlalu banyak terjadi di dalam) dengan jenis lain (mencoba untuk menjaga semua kolaborator / level abstraksi langsung untuk mengetahui apa yang sebenarnya dilakukan kelas-kelas ini).

Jika ragu, tinggalkan saja! Anda selalu dapat refactor nanti, ketika ada kasus yang jelas untuk itu.

Bagaimana menurut anda?

Brandon
sumber
Meskipun pedoman ini mungkin atau mungkin tidak membantu, itu tidak benar-benar ada hubungannya dengan SRP sebagaimana didefinisikan dalam SOLID, bukan?
sara
Terima kasih. Saya tidak dapat mempercayai beberapa kegilaan yang terlibat dalam apa yang disebut prinsip-prinsip SOLID, di mana mereka membuat kode yang sangat sederhana seratus kali lebih kompleks tanpa alasan yang jelas . Poin yang Anda berikan menjelaskan alasan dunia nyata untuk menerapkan SRP. Saya pikir prinsip yang Anda berikan di atas harus menjadi akronim mereka sendiri, dan membuang "SOLID", itu lebih berbahaya daripada kebaikan. "Astronot Arsitektur" memang, seperti yang Anda tunjukkan di utas yang menuntun saya ke sini.
Nicholas Petersen
4

Saya tidak tahu apakah ada skala objektif untuk itu, tetapi apa yang akan memberikan ini akan menjadi metode - bukan jumlah mereka tetapi berbagai fungsi mereka. Saya setuju Anda bisa mengambil dekomposisi terlalu jauh tetapi saya tidak akan mengikuti aturan yang keras dan cepat tentang hal itu.

Jeremy
sumber
4

Jawabannya ada pada definisi

Apa yang Anda definisikan sebagai tanggung jawab, pada akhirnya memberi Anda batasan.

Contoh:

Saya memiliki komponen yang memiliki tanggung jawab untuk menampilkan faktur -> dalam hal ini, jika saya mulai menambahkan sesuatu lebih banyak maka saya melanggar prinsipnya.

Jika di sisi lain jika saya mengatakan bertanggung jawab menangani faktur -> menambahkan beberapa fungsi yang lebih kecil (mis. Mencetak Faktur , memperbarui Faktur ) semuanya berada dalam batas itu.

Namun jika modul itu mulai menangani fungsi apa pun di luar faktur , maka itu akan berada di luar batas itu.

Malam gelap
sumber
Saya kira masalah utama di sini adalah kata "handling". Terlalu umum untuk mendefinisikan satu tanggung jawab. Saya pikir akan lebih baik untuk memiliki komponen yang bertanggung jawab untuk faktur cetak dan satu lagi untuk memperbarui faktur daripada menangani komponen tunggal {cetak, perbarui, dan - mengapa tidak? - tampilan, apa pun} faktur .
Machado
1
OP pada dasarnya bertanya "Bagaimana Anda mendefinisikan tanggung jawab?" jadi ketika Anda mengatakan bahwa tanggung jawab adalah apa pun yang Anda definisikan sepertinya itu hanya mengulangi pertanyaannya.
Despertar
2

Saya selalu melihatnya di dua level:

  • Saya memastikan metode saya hanya melakukan satu hal dan melakukannya dengan baik
  • Saya melihat kelas sebagai pengelompokan logis (OO) dari metode-metode yang mewakili satu hal dengan baik

Jadi sesuatu seperti objek domain bernama Dog:

Dogadalah kelas saya tetapi Anjing dapat melakukan banyak hal! Saya mungkin memiliki metode seperti walk(), run()dan bite(DotNetDeveloper spawnOfBill)(maaf tidak bisa menolak; p).

Jika Dogmenjadi sulit maka saya akan berpikir tentang bagaimana kelompok metode tersebut dapat dimodelkan bersama di kelas lain, seperti Movementkelas, yang bisa berisi metode saya walk()dan run().

Tidak ada aturan yang keras dan cepat, desain OO Anda akan berkembang seiring waktu. Saya mencoba untuk menggunakan antarmuka yang jelas / API publik serta metode sederhana yang melakukan satu hal dan satu hal dengan baik.

Martijn Verburg
sumber
Bite harus benar-benar mengambil instance dari Object, dan DotNetDeveloper harus menjadi subclass dari Person (biasanya, toh!)
Alan Pearce
@Alan - Di sana - memperbaikinya untuk Anda :-)
Martijn Verburg
1

Saya melihatnya lebih sepanjang garis kelas seharusnya hanya mewakili satu hal. Untuk contoh @ Karianna yang sesuai, saya memiliki Dogkelas saya , yang memiliki metode untuk walk(), run()dan bark(). Saya tidak akan menambahkan metode untuk meaow(), squeak(), slither()atau fly()karena mereka tidak hal yang anjing lakukan. Mereka adalah hal-hal yang dilakukan hewan lain, dan hewan-hewan lain itu akan memiliki kelas sendiri untuk mewakili mereka.

(BTW, jika anjing Anda terbang, maka Anda mungkin harus berhenti melemparkannya ke luar jendela).

JohnL
sumber
+1 untuk "jika anjing Anda beterbangan, maka Anda mungkin harus berhenti melemparkannya ke luar jendela". :)
Bobby Tables
Di luar pertanyaan tentang apa yang harus diwakili oleh kelas , apa yang diwakili oleh sebuah instance ? Jika seseorang menganggap SeesFoodsebagai karakteristik dari DogEyes, Barksebagai sesuatu yang dilakukan oleh a DogVoice, dan Eatsebagai sesuatu yang dilakukan oleh a DogMouth, maka logika suka if (dog.SeesFood) dog.Eat(); else dog.Bark();akan menjadi if (eyes.SeesFood) mouth.Eat(); else voice.Bark();, kehilangan rasa identitas bahwa mata, mulut, dan suara semuanya terhubung ke satu kesatuan.
supercat
@supercat itu adalah poin yang adil, meskipun konteksnya penting. Jika kode yang Anda sebutkan adalah di dalam Dogkelas, maka itu mungkin Dogterkait. Jika tidak, maka Anda mungkin akan berakhir dengan sesuatu seperti myDog.Eyes.SeesFoodbukan hanya eyes.SeesFood. Kemungkinan lain adalah yang Dogmengekspos ISeeantarmuka yang menuntut Dog.Eyesproperti dan SeesFoodmetode.
JohnL
@ JohnL: Jika mekanisme penglihatan yang sebenarnya ditangani oleh mata anjing, pada dasarnya sama dengan cara kucing atau zebra, maka mungkin masuk akal jika mekaniknya ditangani oleh Eyekelas, tetapi seekor anjing harus "melihat" menggunakan matanya, bukan hanya memiliki mata yang bisa melihat. Seekor anjing bukan mata, tetapi juga bukan hanya pemegang mata. Ini adalah "hal yang [setidaknya bisa] dilihat", dan harus dideskripsikan sebagai antarmuka seperti itu. Bahkan seekor anjing buta dapat ditanyai apakah ia melihat makanan; itu tidak akan sangat berguna, karena anjing akan selalu berkata "tidak", tetapi tidak ada salahnya bertanya.
supercat
Maka Anda akan menggunakan antarmuka ISee seperti yang saya jelaskan di komentar saya.
JohnL
1

Kelas harus melakukan satu hal ketika dilihat pada tingkat abstraksinya sendiri. Tidak diragukan lagi akan melakukan banyak hal pada tingkat yang kurang abstrak. Beginilah cara kerja kelas untuk membuat program lebih mudah dikelola: mereka menyembunyikan detail implementasi jika Anda tidak perlu memeriksanya dengan cermat.

Saya menggunakan nama kelas sebagai ujian untuk ini. Jika saya tidak bisa memberi kelas nama deskriptif yang cukup pendek, atau jika nama itu memiliki kata seperti "Dan" di dalamnya, saya mungkin melanggar Prinsip Tanggung Jawab Tunggal.

Dalam pengalaman saya, lebih mudah untuk menjaga prinsip ini di tingkat yang lebih rendah, di mana segalanya lebih konkret.

David Thornley
sumber
0

Ini tentang memiliki satu peran unik .

Setiap kelas harus dilanjutkan dengan nama peran. Peran sebenarnya adalah (set) kata kerja yang terkait dengan konteks.

Sebagai contoh :

File menyediakan akses file. FileManager mengelola objek File.

Resource menahan data untuk satu sumber daya dari File. ResourceManager memegang dan menyediakan semua Sumber Daya.

Di sini Anda dapat melihat bahwa beberapa kata kerja seperti "kelola" menyiratkan seperangkat kata kerja lainnya. Kata kerja saja lebih baik dianggap sebagai fungsi daripada kelas, sebagian besar waktu. Jika kata kerja menyiratkan terlalu banyak tindakan yang memiliki konteks bersama sendiri, maka itu harus menjadi kelas itu sendiri.

Jadi, idenya adalah hanya untuk memberi Anda ide sederhana tentang apa yang dilakukan kelas dengan mendefinisikan peran unik, yang mungkin merupakan agregat beberapa sub-peran (yang dilakukan oleh objek anggota atau objek lain).

Saya sering membangun kelas Manajer yang memiliki beberapa kelas lain di dalamnya. Seperti Pabrik, Register, dll. Lihatlah kelas Manajer seperti semacam ketua kelompok, ketua orkestra yang membimbing orang lain untuk bekerja sama untuk mencapai gagasan tingkat tinggi. Dia memiliki satu peran, tetapi menyiratkan bekerja dengan peran unik lainnya di dalam. Anda juga dapat melihatnya seperti bagaimana sebuah perusahaan diorganisasikan: seorang CEO bukan yang produktif pada tingkat produktivitas murni, tetapi jika dia tidak ada di sana, maka tidak ada yang dapat bekerja dengan benar bersama. Itu perannya.

Saat Anda mendesain, kenali peran unik. Dan untuk setiap peran, lihat lagi apakah itu tidak dapat dipotong di beberapa peran lainnya. Dengan begitu, jika Anda perlu mengubah cara Manajer Anda membangun objek, simak saja Pabrik dan pergi dengan tenang.

Klaim
sumber
-1

SRP tidak hanya tentang membagi kelas, tetapi juga tentang mendelegasikan fungsi.

Dalam contoh Dog yang digunakan di atas, jangan gunakan SRP sebagai pembenaran untuk memiliki 3 kelas terpisah seperti DogBarker, DogWalker, dll (kohesi rendah). Sebagai gantinya, lihat implementasi metode kelas dan putuskan apakah mereka "tahu terlalu banyak". Anda masih dapat memiliki dog.walk (), tetapi mungkin metode walk () harus mendelegasikan ke kelas lain rincian tentang cara berjalan.

Akibatnya, kami mengizinkan kelas Anjing memiliki satu alasan untuk berubah: karena Anjing berubah. Tentu saja, menggabungkan ini dengan prinsip-prinsip SOLID lainnya Anda akan Memperpanjang Anjing untuk fungsionalitas baru daripada mengubah Anjing (terbuka / tertutup). Dan Anda akan Menyuntikkan dependensi Anda seperti IMove dan IEat. Dan tentu saja Anda akan membuat antarmuka terpisah ini (segregasi antarmuka). Anjing hanya akan berubah jika kami menemukan bug atau jika Anjing berubah secara mendasar (Sub Liskov, jangan memperpanjang dan kemudian menghapus perilaku).

Efek bersih dari SOLID adalah kita dapat menulis kode baru lebih sering daripada kita harus memodifikasi kode yang ada, dan itu adalah kemenangan besar.

pengguna1488779
sumber
-1

Itu semua tergantung pada definisi tanggung jawab dan bagaimana definisi itu akan berdampak pada pemeliharaan kode Anda. Semuanya bermuara pada satu hal dan itulah bagaimana desain Anda akan membantu Anda dalam menjaga kode Anda.

Dan Seperti seseorang berkata "Berjalan di atas air dan merancang perangkat lunak dari spesifikasi yang diberikan itu mudah, asalkan keduanya dibekukan".

Jadi, jika kita mendefinisikan tanggung jawab dengan cara yang lebih konkret, maka kita tidak perlu mengubahnya.

Kadang-kadang tanggung jawab akan sangat jelas, tetapi kadang-kadang mungkin halus dan kita perlu memutuskan dengan bijaksana.

Misalkan, kita menambahkan tanggung jawab lain ke kelas Dog bernama catchThief (). Sekarang mungkin mengarah ke tanggung jawab tambahan yang berbeda. Besok, jika cara Anjing menangkap pencuri harus diubah oleh Departemen Kepolisian, maka kelas Anjing harus diubah. Akan lebih baik untuk membuat subclass lain dalam kasus ini dan beri nama ThiefCathcerDog. Tetapi dalam pandangan yang berbeda, jika kita yakin bahwa itu tidak akan berubah dalam keadaan apa pun, atau cara catchThief telah dilaksanakan tergantung pada beberapa parameter eksternal, maka sangat baik untuk memiliki tanggung jawab ini. Jika tanggung jawab tidak aneh, maka kita harus memutuskannya secara bijaksana berdasarkan use case.

AKS
sumber
-1

"Satu alasan untuk berubah" tergantung pada siapa yang menggunakan sistem. Pastikan Anda meminta Anda menggunakan case untuk setiap aktor, dan membuat daftar perubahan yang paling mungkin, untuk setiap kemungkinan perubahan use case pastikan hanya satu kelas yang akan terpengaruh oleh perubahan itu. Jika Anda menambahkan kasus penggunaan yang sama sekali baru, pastikan Anda hanya perlu memperpanjang kelas untuk melakukannya.

kiwicomb123
sumber
1
Satu alasan untuk berubah tidak ada hubungannya dengan jumlah kasus penggunaan, aktor, atau seberapa besar kemungkinan perubahan itu terjadi. Anda seharusnya tidak membuat daftar kemungkinan perubahan. Benar bahwa satu perubahan hanya berdampak pada satu kelas. Mampu memperluas kelas untuk mengakomodasi perubahan itu bagus, tapi itu prinsip terbuka, bukan SRP.
candied_orange
Mengapa kita tidak membuat daftar kemungkinan perubahan? Desain spekulatif?
kiwicomb123
karena itu tidak akan membantu, tidak akan lengkap, dan ada cara yang lebih efektif untuk menghadapi perubahan daripada mencoba memprediksinya. Harapkan saja. Mengisolasi keputusan dan dampaknya akan minimal.
candied_orange
Oke, saya mengerti, itu melanggar prinsip "Anda tidak akan membutuhkannya".
kiwicomb123
Sebenarnya yagni bisa didorong hingga jauh juga. Ini benar-benar dimaksudkan agar Anda tidak menerapkan kasus penggunaan spekulatif. Tidak mencegah Anda mengisolasi use case yang diterapkan.
candied_orange