Upewnij się, że każda klasa ma tylko jedną odpowiedzialność, dlaczego?

37

Zgodnie z dokumentacją Microsoft, artykułem Wikipedia SOLID lub większością architektów IT musimy upewnić się, że każda klasa ma tylko jedną odpowiedzialność. Chciałbym wiedzieć, dlaczego, ponieważ jeśli wszyscy wydają się zgadzać z tą zasadą, nikt nie wydaje się zgadzać co do przyczyn tej reguły.

Niektórzy wskazują na lepszą konserwację, inni twierdzą, że zapewnia łatwe testowanie lub czyni klasę bardziej niezawodną lub bezpieczną. Co jest poprawne i co to właściwie znaczy? Dlaczego usprawnia konserwację, testowanie lub kod jest bardziej niezawodny?

Bastien Vandamme
źródło
1
Miałem też pytanie, które możesz uznać za związane: jaka jest prawdziwa odpowiedzialność klasy?
Pierre Arlaud,
3
Czy wszystkie przyczyny pojedynczej odpowiedzialności nie mogą być prawidłowe? Ułatwia to konserwację. Ułatwia to testowanie. To sprawia, że ​​klasa jest bardziej odporna (ogólnie).
Martin York,
2
Z tego samego powodu masz klasy w ogóle.
Davor Ždralo
2
Zawsze miałem problem z tym stwierdzeniem. Bardzo trudno jest zdefiniować, czym jest „pojedyncza odpowiedzialność”. Pojedyncza odpowiedzialność może obejmować dowolne miejsce, od „sprawdzenia, czy 1 + 1 = 2” do „prowadzenia dokładnego rejestru wszystkich pieniędzy wpłacanych na konta firmowe i z nich”.
Dave Nay

Odpowiedzi:

57

Modułowość. Każdy przyzwoity język da ci możliwość sklejenia ze sobą kawałków kodu, ale nie ma ogólnego sposobu na odblokowanie dużego kawałka kodu bez przeprowadzenia przez programistę operacji na źródle. Łącząc wiele zadań w jedną konstrukcję kodu, pozbawiasz siebie i innych możliwości łączenia ich elementów na inne sposoby i wprowadzasz niepotrzebne zależności, które mogą spowodować, że zmiany jednego elementu wpłyną na inne.

SRP ma tak samo zastosowanie do funkcji, jak i do klas, ale główne języki OOP są stosunkowo słabe w łączeniu funkcji razem.

Doval
źródło
12
+1 za wzmiankę o programowaniu funkcjonalnym jako bezpieczniejszym sposobie komponowania abstrakcji.
logc
> ale główne języki OOP są stosunkowo słabe w łączeniu funkcji. <Być może dlatego, że języki OOP nie dotyczą funkcji, ale komunikatów.
Jeff Hubbard
@JeffHubbard Myślę, że masz na myśli to, że biorą po C / C ++. Nic nie ma w przedmiotach, które wykluczają się wzajemnie z funkcjami. Do diabła, obiekt / interfejs to tylko zapis / struktura funkcji. Udawanie, że funkcje nie są ważne, jest nieuzasadnione zarówno w teorii, jak i praktyce. I tak nie da się ich uniknąć - ostatecznie musisz ominąć „Runnable”, „Fabryki”, „Dostawcy” i „Akcje” lub skorzystać z tak zwanych „wzorców” Strategii i Metod Szablonów, a skończyć kilka niepotrzebnych płyt kotłowych, aby wykonać tę samą pracę.
Doval
@Doval Nie, naprawdę mam na myśli, że OOP zajmuje się wiadomościami. Nie oznacza to, że dany język jest lepszy lub gorszy od funkcjonalnego języka programowania przy łączeniu funkcji razem - po prostu, że nie jest to główny problem języka .
Jeff Hubbard
Zasadniczo, jeśli twój język ma na celu ułatwienie / lepsze / szybsze / silniejsze OOP, to na tym nie skupiasz się na tym, czy funkcje ładnie się łączą, czy nie.
Jeff Hubbard
30

Lepsza konserwacja, łatwe testowanie, szybsze usuwanie błędów to tylko (bardzo przyjemne) wyniki zastosowania SRP. Głównym powodem (jak to ujmuje Robert C. Matin) jest:

Klasa powinna mieć jeden i jedyny powód do zmiany.

Innymi słowy, SRP podnosi lokalizację zmiany .

SRP promuje również kod DRY. Tak długo, jak mamy zajęcia, na których spoczywa tylko jedna odpowiedzialność, możemy z nich korzystać w dowolnym miejscu. Jeśli mamy klasę, która ma dwie obowiązki, ale potrzebujemy tylko jednej z nich, a druga przeszkadza, mamy 2 opcje:

  1. Skopiuj i wklej klasę do innej, a może nawet stwórz kolejnego mutanta wielozadaniowego (nieco zwiększ zadłużenie techniczne).
  2. Podziel klasę i stwórz ją taką, jaka powinna być, co może być kosztowne ze względu na szerokie zastosowanie oryginalnej klasy.
Maciej Chałapuk
źródło
1
+1 Za połączenie SRP z OSUSZANIEM.
Mahdi
21

Łatwo jest utworzyć kod, aby rozwiązać konkretny problem. Bardziej skomplikowane jest tworzenie kodu, który rozwiązuje ten problem, jednocześnie umożliwiając bezpieczne wprowadzanie późniejszych zmian. SOLID zapewnia zestaw praktyk, które poprawiają kod.

Co do poprawności: Wszystkie trzy. Są to wszystkie zalety korzystania z pojedynczej odpowiedzialności i powód, dla którego powinieneś z niej korzystać.

Co oznaczają:

  • Lepsza konserwacja oznacza, że ​​łatwiej ją zmienić i nie zmienia się tak często. Ponieważ kodu jest mniej, a kod ten koncentruje się na czymś konkretnym, jeśli chcesz zmienić coś, co nie jest związane z klasą, nie trzeba zmieniać klasy. Ponadto, gdy musisz zmienić klasę, o ile nie musisz zmieniać interfejsu publicznego, musisz się tylko martwić o tę klasę i nic więcej.
  • Łatwe testowanie oznacza mniej testów, mniej konfiguracji. W klasie nie ma tylu ruchomych części, więc liczba możliwych awarii w klasie jest mniejsza, a zatem mniej przypadków, które trzeba przetestować. Będzie mniej prywatnych pól / członków do skonfigurowania.
  • Z powodu dwóch powyższych otrzymujesz klasę, która zmienia się mniej i mniej zawodzi, a zatem jest bardziej niezawodna.

Spróbuj utworzyć kod przez chwilę, zachowując tę ​​zasadę, i wróć do niego później, aby wprowadzić pewne zmiany. Zobaczysz ogromną przewagę, jaką zapewnia.

Robisz to samo dla każdej klasy i otrzymujesz więcej klas, wszystkie zgodne z SRP. Sprawia, że ​​struktura jest bardziej złożona z punktu widzenia połączeń, ale uzasadnia to prostota każdej klasy.

Miyamoto Akira
źródło
Nie sądzę, aby przedstawione tu argumenty były uzasadnione. W szczególności, w jaki sposób unikasz gier o sumie zerowej, w których uproszczenie klasy powoduje, że inne klasy stają się bardziej skomplikowane, bez wpływu netto na bazę kodu jako całość? Wierzę @ Doval za odpowiedź ma rozwiązać ten problem.
2
Robisz to samo dla wszystkich klas. Kończysz więcej klas, wszystkie zgodne z SRP. To sprawia, że ​​struktura jest bardziej złożona z punktu widzenia połączeń. Ale prostota każdej klasy to uzasadnia. Jednak podoba mi się odpowiedź @ Dovala.
Miyamoto Akira,
Myślę, że powinieneś dodać to do swojej odpowiedzi.
4

Oto argumenty, które moim zdaniem potwierdzają twierdzenie, że zasada pojedynczej odpowiedzialności jest dobrą praktyką. Podaję również linki do dalszej literatury, w których można przeczytać jeszcze bardziej szczegółowe uzasadnienia - i bardziej wymowne niż moje:

  • Lepsza konserwacja : idealnie, za każdym razem, gdy funkcjonalność systemu musi się zmienić, będzie jedna i tylko jedna klasa, która musi zostać zmieniona. Jasne przyporządkowanie klas i obowiązków oznacza, że ​​każdy programista zaangażowany w projekt może zidentyfikować, która to klasa. (Jak zauważył @ MaciejChałapuk, patrz Robert C. Martin, „Clean Code” .)

  • Łatwiejsze testowanie : idealnie, klasy powinny mieć jak najmniejszy publiczny interfejs, a testy powinny dotyczyć tylko tego publicznego interfejsu. Jeśli nie możesz przeprowadzić jasnego testu, ponieważ wiele części twojej klasy jest prywatnych, oznacza to wyraźny znak, że twoja klasa ma zbyt wiele obowiązków i że powinieneś podzielić ją na mniejsze podklasy. Zauważ, że dotyczy to również języków, w których nie ma członków klasy „publicznej” lub „prywatnej”; posiadanie małego publicznego interfejsu oznacza, że ​​kod klienta jest bardzo jasny, z których części klasy powinien korzystać. (Aby uzyskać więcej informacji, zobacz Kent Beck, „Test-Driven Development” ).

  • Solidny kod : Twój kod nie zawiedzie się mniej lub bardziej często, ponieważ jest dobrze napisany; ale, jak cały kod, jego ostatecznym celem nie jest komunikowanie się z maszyną , ale z innymi programistami (patrz Kent Beck, „Wzorce implementacji” , rozdział 1.) Łatwiej jest zrozumieć przejrzystą bazę kodu, więc mniej błędów zostanie wprowadzonych , a mniej czasu upłynie między wykryciem błędu a naprawieniem go.

logc
źródło
Jeśli coś musi się zmienić z powodów biznesowych, uwierz mi w SRP, będziesz musiał zmodyfikować więcej niż jedną klasę. Mówienie, że jeśli zmiana klasy nastąpi tylko z jednego powodu, nie jest tym samym, że jeśli nastąpi zmiana, wpłynie to tylko na jedną klasę.
Bastien Vandamme
@ B413: Nie miałem na myśli, że jakakolwiek zmiana będzie oznaczać jedną zmianę w jednej klasie. Wiele części systemu może wymagać zmian w celu dostosowania do jednej zmiany biznesowej. Ale może masz rację i powinienem był napisać „za każdym razem, gdy funkcjonalność systemu musi się zmienić”.
logc
3

Jest wiele powodów, ale podoba mi się podejście stosowane we wczesnych programach UNIX: Zrób jedną rzecz dobrze. Jest to wystarczająco trudne, aby zrobić to z jedną rzeczą, a im trudniej jest robić, tym więcej próbujesz robić.

Innym powodem jest ograniczenie i kontrolowanie skutków ubocznych. Uwielbiałem mój otwieracz do drzwi z ekspresem do kawy. Niestety kawa zwykle przelewała się, gdy miałem gości. Zapomniałem zamknąć drzwi po tym, jak zrobiłem kawę pewnego dnia i ktoś ją ukradł.

Z psychologicznego punktu widzenia możesz śledzić tylko kilka rzeczy na raz. Ogólne szacunki to siedem plus minus dwa. Jeśli klasa robi wiele rzeczy, musisz śledzić je wszystkie naraz. Zmniejsza to twoją zdolność do śledzenia tego, co robisz. Jeśli klasa robi trzy rzeczy i chcesz tylko jedną z nich, możesz wyczerpać swoją zdolność do śledzenia rzeczy, zanim zrobisz cokolwiek z klasą.

Robienie wielu rzeczy zwiększa złożoność kodu. Oprócz najprostszego kodu rosnąca złożoność zwiększa prawdopodobieństwo błędów. Z tego punktu widzenia chcesz, aby zajęcia były jak najprostsze.

Testowanie klasy, która wykonuje jedną rzecz, jest znacznie prostsze. Nie musisz sprawdzać, czy druga rzecz, którą klasa zrobiła lub nie zdarzyła się przy każdym teście. Nie musisz również naprawiać uszkodzonych warunków i ponownie testować, gdy jeden z tych testów zakończy się niepowodzeniem.

BillThor
źródło
2

Ponieważ oprogramowanie jest organiczne. Wymagania stale się zmieniają, więc musisz manipulować komponentami przy jak najmniejszym bólu głowy. Nieprzestrzeganie zasad SOLID może spowodować, że baza kodu będzie konkretna.

Wyobraź sobie dom z betonową ścianą nośną. Co się stanie, gdy wyjmiesz tę ścianę bez żadnego wsparcia? Dom prawdopodobnie się zawali. W oprogramowaniu nie chcemy tego, więc konstruujemy aplikacje w taki sposób, abyś mógł łatwo przenosić / wymieniać / modyfikować komponenty bez powodowania dużych szkód.

CodeART
źródło
1

Śledzę tę myśl: 1 Class = 1 Job.

Korzystanie z analogii fizjologii: silnik (układ nerwowy), oddychanie (płuca), pokarmowy (żołądek), węchowy (obserwacja) itp. Każdy z nich będzie miał podzbiór kontrolerów, ale każdy z nich ma tylko 1 odpowiedzialność, niezależnie od tego, czy ma zarządzać sposób, w jaki działa każdy z ich podsystemów lub czy są one podsystemem punktu końcowego i wykonują tylko jedno zadanie, takie jak podniesienie palca lub wyhodowanie mieszków włosowych.

Nie należy mylić faktu, że może on działać jako kierownik zamiast pracownika. Niektórzy pracownicy ostatecznie awansują na kierownika, gdy praca, którą wykonują, stała się zbyt skomplikowana, aby jeden proces mógł sam sobie poradzić.

Najbardziej skomplikowaną częścią tego, czego doświadczyłem, jest wiedza, kiedy wyznaczyć klasę jako operatora, przełożonego lub kierownika. Niezależnie od tego musisz obserwować i oznaczać jego funkcjonalność dla 1 odpowiedzialności (Operator, Kierownik lub Kierownik).

Gdy klasa / obiekt wykonuje więcej niż jedną z tych ról typu, przekonasz się, że w całym procesie zaczną występować problemy z wydajnością lub będą występować wąskie gardła.

GoldBishop
źródło
Nawet jeśli implementacja przetwarzania tlenu atmosferycznego w hemoglobinę powinna być traktowana jako Lungklasa, uważam, że instancja Personpowinna być nadal Breathe, a zatem Personklasa powinna zawierać wystarczającą logikę, aby przynajmniej przekazać obowiązki związane z tą metodą. Sugerowałbym ponadto, że las połączonych ze sobą podmiotów, które są dostępne tylko za pośrednictwem wspólnego właściciela, jest często łatwiejszy do uzasadnienia niż las, który ma wiele niezależnych punktów dostępu.
supercat
Tak, w takim przypadku Płuco jest Kierownikiem, chociaż Villi byłby Opiekunem, a proces Oddychania byłby klasą Robotniczą, która przenosiłaby cząsteczki Powietrza do strumienia Krwi.
GoldBishop
@GoldBishop: Być może właściwym poglądem byłoby powiedzenie, że Personklasa ma za zadanie delegowanie całej funkcjonalności związanej z monstrualnie nadmiernie skomplikowanym bytem „prawdziwym człowiekiem”, w tym skojarzeniem jego różnych części . Posiadanie jednostki, która robi 500 rzeczy, jest brzydkie, ale jeśli tak właśnie działa modelowany system rzeczywisty, posiadanie klasy, która deleguje 500 funkcji przy zachowaniu jednej tożsamości, może być lepsze niż robienie wszystkiego z rozłącznymi kawałkami.
supercat
@ supercat Lub możesz po prostu podzielić na części całość i mieć 1 klasę z niezbędnym Opiekunem nad podprocesami. W ten sposób możesz (teoretycznie) oddzielić każdy proces od klasy podstawowej, Personale nadal raportować w górę łańcucha o sukcesie / porażce, ale niekoniecznie wpływając na drugi. 500 funkcji (choć podane jako przykład, byłyby za burtą i nie byłyby obsługiwane) staram się zachować tego typu funkcjonalność pochodną i modułową.
GoldBishop
@GoldBishop: Chciałbym, aby istniał jakiś sposób na zadeklarowanie typu „efemerycznego”, tak aby zewnętrzny kod mógł wywoływać członków lub przekazywać go jako parametr do własnych metod tego typu, ale nic więcej. Z punktu widzenia organizacji kodu dzielenie klas ma sens, a nawet posiadanie zewnętrznych metod wywoływania metod o jeden poziom niżej (np. Ma Fred.vision.lookAt(George)sens, ale pozwolenie, by kod powiedział, że someEyes = Fred.vision; someTubes = Fred.digestionwydaje się trudne, ponieważ przesłania związek między someEyesa someTubes.
supercat
1

Zwłaszcza w przypadku tak ważnej zasady, jak pojedyncza odpowiedzialność, osobiście oczekiwałbym, że istnieje wiele powodów, dla których ludzie przyjmują tę zasadę.

Niektóre z tych przyczyn mogą być:

  • Utrzymanie - SRP zapewnia, że ​​zmiana odpowiedzialności w jednej klasie nie wpływa na inne obowiązki, co upraszcza utrzymanie. Jest tak, ponieważ jeśli każda klasa ma tylko jedną odpowiedzialność, zmiany dokonane w jednej odpowiedzialności są odizolowane od innych obowiązków.
  • Testowanie - jeśli klasa ma jedną odpowiedzialność, znacznie łatwiej jest dowiedzieć się, jak ją przetestować. Jeśli klasa ma wiele obowiązków, musisz upewnić się, że testujesz prawidłową i że na test nie mają wpływu inne obowiązki, które ma klasa.

Należy również pamiętać, że SRP zawiera pakiet zasad SOLID. Przestrzeganie SRP i ignorowanie reszty jest tak samo złe, jak nie wykonywanie SRP. Dlatego nie powinieneś oceniać SRP sam w sobie, ale w kontekście wszystkich zasad SOLID.

Euforyk
źródło
... test is not affected by other responsibilities the class hasczy mógłbyś rozwinąć tę kwestię?
Mahdi
1

Najlepszym sposobem na zrozumienie znaczenia tych zasad jest potrzeba.

Kiedy byłem początkującym programistą, nie myślałem o projektowaniu, w rzeczywistości nawet nie wiedziałem, że istnieją wzorce projektowe. W miarę rozwoju moich programów zmiana jednej rzeczy oznaczała zmianę wielu innych rzeczy. Trudno było wyśledzić błędy, kod był ogromny i powtarzalny. Hierarchia obiektów była niewielka, wszystko było wszędzie. Dodanie czegoś nowego lub usunięcie czegoś starego spowodowałoby błędy w innych częściach programu. Domyśl.

W małych projektach może to nie mieć znaczenia, ale w dużych projektach może być bardzo koszmarnie. Później, kiedy natknąłem się na koncepcje wzorów projektowych, powiedziałem sobie: „o tak, zrobienie tego znacznie ułatwiłoby to wtedy”.

Naprawdę nie możesz zrozumieć znaczenia wzorców projektowych, dopóki nie pojawi się taka potrzeba. Szanuję wzorce, ponieważ z doświadczenia mogę powiedzieć, że sprawiają, że utrzymanie kodu jest łatwe i niezawodne.

Jednak, podobnie jak ty, wciąż nie jestem pewien co do „łatwych testów”, ponieważ nie miałem jeszcze potrzeby wchodzenia w testy jednostkowe.

harsimranb
źródło
Wzory są w jakiś sposób połączone z SRP, ale SRP nie jest wymagane, gdy stosuje się wzorce projektowe. Można używać wzorców i całkowicie ignorować SRP. Używamy wzorców w celu spełnienia wymagań niefunkcjonalnych, ale używanie wzorców nie jest wymagane w żadnym programie. Zasady są niezbędne, aby uczynić rozwój mniej bolesnym i (IMO) powinno być nawykiem.
Maciej Chałapuk
Całkowicie się z Tobą zgadzam. SRP, do pewnego stopnia, wymusza czyste projektowanie i hierarchię obiektów. Jest to dobra mentalność dla programisty, ponieważ pozwala programistom zacząć myśleć o obiektach i tym, jak powinny być. Osobiście miało to również bardzo dobry wpływ na mój rozwój.
harsimranb
1

Odpowiedź jest, jak zauważyli inni, wszystkie z nich są poprawne i wszystkie wzajemnie się łączą, łatwiej przetestować ułatwia konserwację sprawia, że ​​kod jest bardziej niezawodny, ułatwia konserwację i tak dalej ...

Wszystko sprowadza się do kluczowej zasady - kod powinien być tak mały i robić tak mało, jak to konieczne, aby wykonać zadanie. Dotyczy to aplikacji, pliku lub klasy tak samo jak funkcji. Im więcej rzeczy robi dany fragment kodu, tym trudniej jest go zrozumieć, utrzymać, rozszerzyć lub przetestować.

Myślę, że można to podsumować jednym słowem: zakres. Zwróć szczególną uwagę na zakres artefaktów, im mniej elementów w danym punkcie aplikacji, tym lepiej.

Rozszerzony zakres = większa złożoność = więcej sposobów, aby coś poszło nie tak.

jmoreno
źródło
1

Jeśli klasa jest zbyt duża, trudno jest ją utrzymać, przetestować i zrozumieć, inne odpowiedzi obejmowały tę wolę.

Możliwe jest, że klasa będzie miała więcej niż jedną odpowiedzialność bez problemów, ale wkrótce natrafisz na problemy ze zbyt złożonymi klasami.

Jednak posiadanie prostej zasady „tylko jedna odpowiedzialność” sprawia, że łatwiej jest wiedzieć, kiedy potrzebujesz nowej klasy .

Jakkolwiek zdefiniowanie „odpowiedzialności” jest trudne, nie oznacza to „robienia wszystkiego, co mówi specyfikacja aplikacji”, prawdziwą umiejętnością jest umieć rozłożyć problem na małe jednostki „odpowiedzialności”.

Ian
źródło