Wyjaśnij zasadę jednej odpowiedzialności

64

Zasada Jednej Odpowiedzialności stanowi, że klasa powinna zrobić jedną i tylko jedną rzecz. Niektóre przypadki są dość wyraźne. Inne są jednak trudne, ponieważ to, co wygląda na „jedną rzecz”, gdy jest oglądane na danym poziomie abstrakcji, może być wieloma rzeczami, gdy patrzy się na niższym poziomie. Obawiam się również, że jeśli zasada niższej odpowiedzialności będzie przestrzegana na niższych poziomach, może to spowodować nadmiernie oddzielony, pełny kod ravioli, w którym wydano więcej linii, tworząc małe klasy dla wszystkiego i gromadząc informacje wokół, niż faktycznie rozwiązując dany problem.

Jak opisałbyś, co oznacza „jedna rzecz”? Jakie są konkretne oznaki, że klasa naprawdę robi więcej niż „jedną rzecz”?

dsimcha
źródło
6
+1 za ponad „kod ravioli”. Na początku mojej kariery byłem jedną z tych osób, które posunęły się za daleko. Nie tylko z klasami, ale także z modularyzacją metod. Mój kod był przesycony mnóstwem małych metod, które zrobiły coś prostego, tylko w celu podzielenia problemu na małe fragmenty, które mogłyby zmieścić się na ekranie bez przewijania. Oczywiście często było to zbyt daleko.
Tabele Bobby'ego

Odpowiedzi:

50

Bardzo podoba mi się sposób, w jaki Robert C. Martin (wujek Bob) przekształca zasadę pojedynczej odpowiedzialności (link do pliku PDF) :

Klasa nigdy nie powinna mieć więcej niż jednego powodu do zmiany

Różni się nieco od tradycyjnej definicji „powinien robić tylko jedną rzecz” i podoba mi się to, ponieważ zmusza cię do zmiany sposobu myślenia o swojej klasie. Zamiast myśleć o „czy to robi jedną rzecz?”, Zamiast tego zastanawiasz się, co można zmienić i jak zmiany te wpływają na twoją klasę. Na przykład, jeśli zmienia się baza danych, czy Twoja klasa musi się zmienić? Co jeśli zmieni się urządzenie wyjściowe (na przykład ekran, urządzenie mobilne lub drukarka)? Jeśli twoja klasa musi się zmienić z powodu zmian z wielu innych kierunków, oznacza to, że twoja klasa ma zbyt wiele obowiązków.

W powiązanym artykule wujek Bob podsumowuje:

SRP jest jedną z najprostszych zasad i jedną z najtrudniejszych do rozwiązania. Łączenie obowiązków to coś, co robimy naturalnie. Znalezienie i oddzielenie tych obowiązków od siebie jest w dużej mierze tym, na czym naprawdę polega projektowanie oprogramowania.

Paddyslacker
źródło
2
Podoba mi się też sposób, w jaki on to stwierdza, wydaje się, że łatwiej jest sprawdzić, kiedy dotyczy zmiany, niż abstrakcyjną „odpowiedzialność”.
Matthieu M.,
To właściwie doskonały sposób na wyrażenie tego. Uwielbiam to. Dla przypomnienia, zwykle myślę o SRP jako o znacznie silniejszym zastosowaniu do metod. Czasami klasa musi po prostu zrobić dwie rzeczy (być może to klasa łączy dwie domeny), ale metoda prawie nigdy nie powinna robić więcej niż to, co można zwięźle opisać za pomocą podpisu typu.
CodexArcanum
1
Właśnie pokazałem to mojemu Gradowi - świetna lektura i cholernie dobre przypomnienie dla mnie.
Martijn Verburg,
1
Ok, to ma sens, gdy połączysz go z ideą, że nieinżynierski kod powinien planować tylko zmiany, które są prawdopodobne w dającej się przewidzieć przyszłości, a nie każdą możliwą zmianę. Chciałbym zatem powtórzyć to nieco jako, że „powinien istnieć tylko jeden powód zmiany klasy, która prawdopodobnie nastąpi w dającej się przewidzieć przyszłości”. Zachęca to do faworyzowania prostoty części projektu, które raczej nie zmienią się, i oddzielenia części, które mogą ulec zmianie.
dsimcha
18

Wciąż zadaję sobie pytanie, jaki problem SRP próbuje rozwiązać? Kiedy SRP mi pomaga? Oto, co wymyśliłem:

Powinieneś refaktoryzować odpowiedzialność / funkcjonalność poza klasę, gdy:

1) Zduplikowano funkcjonalność (DRY)

2) Stwierdzasz, że Twój kod wymaga innego poziomu abstrakcji, aby pomóc Ci go zrozumieć (KISS)

3) Odkrywasz, że eksperci w dziedzinie rozumieją elementy funkcjonalności jako oddzielne od innego komponentu (język wszechobecny)

NIE POWINNYŚ refaktoryzować odpowiedzialności poza klasę, gdy:

1) Nie ma żadnej zduplikowanej funkcjonalności.

2) Ta funkcjonalność nie ma sensu poza kontekstem twojej klasy. Innymi słowy, twoja klasa zapewnia kontekst, w którym łatwiej jest zrozumieć funkcjonalność.

3) Eksperci w Twojej dziedzinie nie mają pojęcia o tej odpowiedzialności.

Przyszło mi do głowy, że jeśli SRP ma szerokie zastosowanie, handlujemy jednym rodzajem złożoności (próbując tworzyć głowy lub ogony klasy o wiele za dużo dzieje się w środku) z innym rodzajem (starając się utrzymać wszystkich współpracowników / poziomy abstrakcja prosto, aby dowiedzieć się, co faktycznie robią te klasy).

W razie wątpliwości pomiń to! Zawsze możesz refaktoryzować później, gdy jest ku temu wyraźny argument.

Co myślisz?

Brandon
źródło
Chociaż te wytyczne mogą, ale nie muszą być pomocne, tak naprawdę nie ma to nic wspólnego z SRP zdefiniowanym w SOLID, prawda?
sara,
Dziękuję Ci. Nie mogę uwierzyć w część szaleństwa związanego z tak zwanymi zasadami SOLID, w których bardzo prosty kod jest sto razy bardziej skomplikowany bez powodu . Podane punkty opisują rzeczywiste powody, dla których warto wdrożyć SRP. Uważam, że zasady, które podałeś powyżej, powinny stać się ich własnym akronimem i wyrzucić „SOLID”, wyrządza więcej szkody niż pożytku. „Astronauci architektury” rzeczywiście, jak wskazałeś w wątku, który mnie tu prowadzi.
Nicholas Petersen
4

Nie wiem, czy istnieje obiektywna skala, ale tym, co by to ujawniło, byłyby metody - nie tyle ich liczba, ile różnorodność ich funkcji. Zgadzam się, że możesz posunąć się za daleko, ale nie przestrzegałbym żadnych twardych i szybkich zasad w tym zakresie.

Jeremy
źródło
4

Odpowiedź leży w definicji

To, co definiujesz jako odpowiedzialność, ostatecznie wyznacza granicę.

Przykład:

Mam komponent, który jest odpowiedzialny za wyświetlanie faktur -> w tym przypadku, jeśli zacznę dodawać coś więcej, to łamię zasadę.

Z drugiej strony, jeśli powiedziałbym, że jestem odpowiedzialny za obsługę faktur -> dodanie wielu mniejszych funkcji (np. Drukowanie faktury , aktualizacja faktury ) wszystkie mieszczą się w tej granicy.

Gdyby jednak ten moduł zaczął obsługiwać dowolną funkcjonalność poza fakturami , byłby poza tą granicą.

Ciemna noc
źródło
Myślę, że głównym problemem tutaj jest słowo „obsługa”. Jest to zbyt ogólne, aby zdefiniować jedną odpowiedzialność. Myślę, że lepiej byłoby mieć komponent odpowiedzialny za wydruk faktury, a inny za fakturę aktualizacyjną niż uchwyt pojedynczego komponentu {drukuj, aktualizuj i - dlaczego nie? - wystawiać, cokolwiek} fakturę .
Machado
1
OP zasadniczo pyta: „Jak określasz odpowiedzialność?” więc kiedy powiesz, że odpowiedzialność jest taka, jak ją zdefiniujesz, wydaje się, że to tylko powtarzanie pytania.
Despertar,
2

Zawsze oglądam to na dwóch poziomach:

  • Upewniam się, że moje metody robią tylko jedno i robią to dobrze
  • Widzę klasę jako logiczne (OO) grupowanie tych metod, które dobrze reprezentuje jedną rzecz

Więc coś w rodzaju obiektu domeny o nazwie Dog:

Dogjest moja klasa, ale Psy potrafią robić wiele rzeczy! Mogę mieć metod, takich jak walk(), run()i bite(DotNetDeveloper spawnOfBill)(Niestety nie może oprzeć się, p).

Jeśli stanie Dogsię nieporęczny, pomyślę o tym, jak grupy tych metod mogą być modelowane razem w innej klasie, takiej jak Movementklasa, która może zawierać moje walk()i run()metody.

Nie ma twardej i szybkiej reguły, twój projekt OO będzie ewoluował z czasem. Staram się korzystać z przejrzystego interfejsu / publicznego interfejsu API, a także prostych metod, które robią jedną i drugą rzecz dobrze.

Martijn Verburg
źródło
Zgryz powinien naprawdę wziąć obiekt Object, a DotNetDeveloper powinien być podklasą Person (zwykle w każdym razie!)
Alan Pearce
@Alan - Tam - naprawiłem to dla ciebie :-)
Martijn Verburg
1

Patrzę na to bardziej zgodnie z klasą, powinna reprezentować tylko jedną rzecz. Przywłaszczyć @ przykład Karianna za, mam Dogklasę, która ma metody walk(), run()i bark(). I nie zamierzam dodać metody meaow(), squeak(), slither()lub fly()ponieważ nie są to rzeczy, które psy robić. Są to rzeczy, które robią inne zwierzęta, a te inne zwierzęta miałyby swoje własne klasy reprezentujące je.

(BTW, jeśli pies ma latać, to prawdopodobnie powinien zatrzymać wyrzucanie go z okna).

JohnL
źródło
+1 za „jeśli twój pies lata, prawdopodobnie powinieneś przestać go wyrzucać przez okno”. :)
Tabele Bobby
Oprócz pytania o to, co klasa powinna reprezentować, co reprezentuje instancja ? Jeśli ktoś uważa SeesFoodza cechę DogEyes, Barkjak coś zrobionego przez DogVoice, i Eatjak coś zrobionego przez DogMouth, wtedy logika if (dog.SeesFood) dog.Eat(); else dog.Bark();stanie się if (eyes.SeesFood) mouth.Eat(); else voice.Bark();, tracąc jakiekolwiek poczucie tożsamości, że oczy, usta i głos są powiązane z jednym uprawnieniem.
supercat
@supercat to uczciwa kwestia, choć kontekst jest ważny. Jeśli wspomniany kod należy do Dogklasy, to prawdopodobnie jest on Dogpowiązany. Jeśli nie, prawdopodobnie skończyłbyś z czymś takim, myDog.Eyes.SeesFooda nie tylko eyes.SeesFood. Inną możliwością jest Dogudostępnienie ISeeinterfejsu wymagającego Dog.Eyeswłaściwości i SeesFoodmetody.
JohnL
@JohnL: Jeśli rzeczywistą mechaniką widzenia zajmują się oczy psa, zasadniczo w taki sam sposób, jak oczy kota lub zebry, może mieć sens, aby mechanika była obsługiwana przez Eyeklasę, ale pies powinien „widzieć” używając jego oczu, a nie tylko oczu, które mogą widzieć. Pies nie jest okiem, ale też nie jest po prostu oprawą oka. Jest to „rzecz, która może [przynajmniej spróbować] zobaczyć” i jako taka powinna zostać opisana za pomocą interfejsu. Nawet ślepego psa można zapytać, czy widzi jedzenie; nie będzie to bardzo przydatne, ponieważ pies zawsze powie „nie”, ale nie ma nic złego w pytaniu.
supercat
Wtedy użyjesz interfejsu ISee, tak jak opisałem w moim komentarzu.
JohnL,
1

Klasa powinna zrobić jedną rzecz, patrząc na swój własny poziom abstrakcji. Bez wątpienia zrobi wiele rzeczy na mniej abstrakcyjnym poziomie. W ten sposób działają klasy, aby programy były łatwiejsze w utrzymaniu: ukrywają szczegóły implementacji, jeśli nie trzeba ich dokładnie badać.

Używam do tego nazw klas. Jeśli nie mogę nadać klasie dość krótkiej nazwy opisowej lub jeśli w nazwie tej znajduje się słowo „I”, prawdopodobnie naruszam zasadę pojedynczej odpowiedzialności.

Z mojego doświadczenia wynika, że ​​łatwiej jest utrzymać tę zasadę na niższych poziomach, gdzie rzeczy są bardziej konkretne.

David Thornley
źródło
0

Chodzi o posiadanie jednego wyjątkowego rola .

Każda klasa powinna zostać wznowiona nazwą roli. Rola to w rzeczywistości (zestaw) czasowników powiązanych z kontekstem.

Na przykład :

Plik zapewnia dostęp do pliku. FileManager zarządza obiektami File.

Dane wstrzymania zasobów dla jednego zasobu z pliku. ResourceManager wstrzymuje i udostępnia wszystkie zasoby.

Tutaj możesz zobaczyć, że niektóre czasowniki, takie jak „zarządzaj”, oznaczają zestaw innych czasowników. Same czasowniki są przez większość czasu lepiej uważane za funkcje niż klasy. Jeśli czasownik implikuje zbyt wiele działań, które mają swój wspólny kontekst, to powinien być klasą samą w sobie.

Chodzi więc o to, abyś miał proste wyobrażenie o tym, co robi klasa, poprzez zdefiniowanie unikalnej roli, która może być agregacją kilku podgrup (wykonywanych przez obiekty składowe lub inne obiekty).

Często buduję klasy menedżerów, które zawierają kilka różnych innych klas. Jak fabryka, rejestr itp. Zobacz klasę menedżera, taką jak szef grupy, szef orkiestry, który poprowadzi inne ludy do współpracy, aby osiągnąć pomysł na wysokim poziomie. Ma jedną rolę, ale implikuje pracę z innymi unikalnymi rolami w środku. Możesz także zobaczyć, jak jest zorganizowana firma: dyrektor generalny nie jest produktywny na poziomie czystej wydajności, ale jeśli go nie ma, nic nie może działać poprawnie razem. To jego rola.

Podczas projektowania określ unikalne role. I dla każdej roli ponownie sprawdź, czy nie można jej rozdzielić na kilka innych ról. W ten sposób, jeśli chcesz w prosty sposób zmienić sposób, w jaki Twój Menedżer buduje obiekty, po prostu zmień Fabrykę i idź spokojnie.

Klaim
źródło
-1

SRP to nie tylko podział klas, ale także delegowanie funkcjonalności.

W powyższym przykładzie Dog nie używaj SRP jako uzasadnienia posiadania 3 oddzielnych klas, takich jak DogBarker, DogWalker itp. (Niska kohezja). Zamiast tego spójrz na implementację metod klasy i zdecyduj, czy „wiedzą za dużo”. Nadal możesz mieć funkcję dog.walk (), ale prawdopodobnie metoda walk () powinna przekazać innej klasie szczegółowe informacje o sposobie chodzenia.

W efekcie pozwalamy klasie psów mieć jeden powód do zmiany: ponieważ zmieniają się psy. Oczywiście, łącząc to z innymi SOLIDNYMI zasadami, rozszerzyłbyś Psa o nową funkcjonalność zamiast zmieniać Psa (otwarty / zamknięty). I wstrzyknąłbyś swoje zależności, takie jak IMove i IEat. I oczywiście stworzyłbyś te oddzielne interfejsy (segregacja interfejsów). Pies zmieniłby się tylko, gdybyśmy znaleźli błąd lub gdyby psy uległy zasadniczej zmianie (Liskov Sub, nie przedłużaj, a następnie usuwaj zachowanie).

Efektem netto SOLID jest to, że możemy pisać nowy kod częściej niż musimy modyfikować istniejący kod, i to jest duża wygrana.

użytkownik1488779
źródło
-1

Wszystko zależy od definicji odpowiedzialności i tego, jak ta definicja wpłynie na utrzymanie twojego kodu. Wszystko sprowadza się do jednej rzeczy i właśnie w ten sposób twój projekt pomoże ci utrzymać kod.

I jak ktoś powiedział: „Chodzenie po wodzie i projektowanie oprogramowania na podstawie podanych specyfikacji jest łatwe, pod warunkiem, że oba są zamrożone”.

Jeśli więc definiujemy odpowiedzialność w bardziej konkretny sposób, nie musimy jej zmieniać.

Czasami obowiązki będą rażąco oczywiste, ale czasem mogą być subtelne i musimy rozsądnie zdecydować.

Załóżmy, że dodajemy kolejną odpowiedzialność do klasy Dog o nazwie catchThief (). Teraz może to prowadzić do dodatkowej różnej odpowiedzialności. Jutro, jeśli sposób, w jaki Pies łapie złodzieja, musi zostać zmieniony przez Departament Policji, klasa psów będzie musiała zostać zmieniona. W tym przypadku lepiej byłoby utworzyć kolejną podklasę i nazwać ją ThiefCathcerDog. Ale w innym ujęciu, jeśli jesteśmy pewni, że nie zmieni się to w żadnych okolicznościach lub sposób, w jaki został wdrożony catchThief, zależy od jakiegoś zewnętrznego parametru, to jest całkowicie w porządku, aby ponosić tę odpowiedzialność. Jeśli obowiązki nie są uderzająco dziwne, musimy podjąć rozsądną decyzję na podstawie przypadku użycia.

AKS
źródło
-1

„Jeden powód do zmiany” zależy od tego, kto korzysta z systemu. Upewnij się, że masz przypadki użycia dla każdego aktora, i zrób listę najbardziej prawdopodobnych zmian, dla każdej możliwej zmiany przypadku użycia upewnij się, że zmiana wpłynie tylko na jedną klasę. Jeśli dodajesz zupełnie nowy przypadek użycia, upewnij się, że będziesz musiał jedynie rozszerzyć klasę, aby to zrobić.

kiwicomb123
źródło
1
Jeden z powodów zmiany nie ma nic wspólnego z liczbą przypadków użycia, aktorów ani prawdopodobieństwem zmiany. Nie powinieneś robić listy prawdopodobnych zmian. To prawda, że ​​jedna zmiana powinna wpłynąć tylko na jedną klasę. Możliwość rozszerzenia klasy w celu uwzględnienia tej zmiany jest dobra, ale to zasada otwartego zamkniętego, a nie SRP.
candied_orange
Dlaczego nie powinniśmy robić listy najbardziej prawdopodobnych zmian? Projekt spekulacyjny?
kiwicomb123
ponieważ to nie pomoże, nie będzie kompletne, a istnieją bardziej skuteczne sposoby radzenia sobie ze zmianami niż próby ich przewidywania. Spodziewaj się tego. Wyodrębnij decyzje, a wpływ będzie minimalny.
candied_orange
Okay, rozumiem, to narusza zasadę „nie potrzebujesz go”.
kiwicomb123
W rzeczywistości yagni można również zepchnąć na odległość. To naprawdę ma na celu powstrzymanie cię od implementowania spekulacyjnych przypadków użycia. Nie powstrzymywać cię przed izolowaniem zaimplementowanego przypadku użycia.
candied_orange