Jak klasa może mieć wiele metod bez naruszania zasady pojedynczej odpowiedzialności

64

Zasada pojedynczej odpowiedzialności jest zdefiniowana w Wikipedii jako

Zasada pojedynczej odpowiedzialności jest zasadą programowania komputerowego, która stwierdza, że ​​każdy moduł, klasa lub funkcja powinna ponosić odpowiedzialność za jedną część funkcjonalności zapewnianej przez oprogramowanie, a odpowiedzialność powinna być całkowicie zamknięta w klasie

Jeśli klasa powinna ponosić tylko jedną odpowiedzialność, jak może mieć więcej niż jedną metodę? Czy każda metoda nie miałaby innej odpowiedzialności, co oznaczałoby, że klasa miałaby więcej niż 1 odpowiedzialność.

Każdy przykład, który widziałem demonstrując zasadę pojedynczej odpowiedzialności, wykorzystuje przykładową klasę, która ma tylko jedną metodę. Pomocne może być zobaczenie przykładu lub wyjaśnienie klasy za pomocą wielu metod, które nadal można uznać za jedną odpowiedzialność.

Gęś
źródło
11
Dlaczego głosowanie negatywne? Wydaje się idealnym pytaniem dla SE.SE; osoba badała ten temat i dołożyła starań, aby pytanie było bardzo jasne. Zamiast tego zasługuje na aprobatę.
Arseni Mourzenko
19
Downvote było prawdopodobnie spowodowane tym, że jest pytanie, które zostało zadane i odpowiedzi już kilkakrotnie, na przykład zobaczyć softwareengineering.stackexchange.com/questions/345018/... . Moim zdaniem nie dodaje istotnych nowych aspektów.
Hans-Martin Mosner
11
Możliwy duplikat zasady „pojedynczej odpowiedzialności”, co stanowi „odpowiedzialność”?
Bart van Ingen Schenau
9
Jest to po prostu reductio ad absurdum. Gdyby każdej klasie dosłownie dozwolono tylko jedną metodę, to dosłownie nie ma mowy, aby jakikolwiek program byłby w stanie zrobić więcej niż jedną rzecz.
Darrel Hoffman
6
@DarrelHoffman To nieprawda. Jeśli każda klasa była funktorem posiadającym tylko metodę „call ()”, to w zasadzie właśnie emulowałeś zwykłe programowanie proceduralne z programowaniem obiektowym. Nadal możesz zrobić wszystko, co mógłbyś zrobić inaczej, ponieważ metoda „call ()” klasy może wywoływać wiele metod „call ()” innych klas.
Vaelus

Odpowiedzi:

29

Jedna odpowiedzialność może nie być czymś, co może spełnić jedna funkcja.

 class Location { 
     public int getX() { 
         return x;
     } 
     public int getY() { 
         return y; 
     } 
 }

Ta klasa może złamać zasadę jednej odpowiedzialności. Nie dlatego, że ma dwie funkcje, ale jeśli koduje getX()i getY()musi zadowolić różnych interesariuszy, którzy mogą wymagać zmian. Jeśli wiceprezydent Pan X rozsyła notatkę, że wszystkie liczby powinny być wyrażone jako liczby zmiennoprzecinkowe, a dyrektor ds. Rachunkowości pani Y nalega, aby wszystkie liczby, które ocenia jej departament, pozostały liczbami całkowitymi, niezależnie od tego, co pan X uważa za dobre, to ta klasa powinna mieć jeden pomysł na to, za kogo jest on odpowiedzialny, ponieważ sprawy stają się mylące.

Gdyby przestrzegano SRP, byłoby jasne, czy klasa Lokacji przyczynia się do rzeczy, na które narażony jest Pan X i jego grupa. Wyjaśnij, za co klasa jest odpowiedzialna, a wiesz, która dyrektywa wpływa na tę klasę. Jeśli oba wpływają na tę klasę, to została źle zaprojektowana, aby zminimalizować wpływ zmiany. „Klasa powinna mieć tylko jeden powód do zmiany” nie oznacza, że ​​cała klasa może zrobić tylko jedną małą rzecz. Oznacza to, że nie powinienem być w stanie spojrzeć na klasę i powiedzieć, że zarówno pan X, jak i pani Y są zainteresowani tą klasą.

Inne niż takie rzeczy. Nie, wiele metod jest w porządku. Po prostu nadaj mu nazwę, która wyjaśnia, które metody należą do klasy, a które nie.

SRP wuja Boba bardziej dotyczy prawa Conwaya niż prawa Curly'ego . Wujek Bob opowiada się za zastosowaniem prawa Curly'ego (zrób jedną rzecz) do funkcji, a nie klas. SRP przestrzega przed mieszaniem powodów do wspólnej zmiany. Prawo Conwaya mówi, że system będzie śledził przepływ informacji w organizacji. To prowadzi do przestrzegania SRP, ponieważ nie obchodzi Cię to, o czym nigdy nie słyszysz.

„Moduł powinien być odpowiedzialny przed jednym i tylko jednym aktorem”

Robert C Martin - Czysta architektura

Ludzie wciąż chcą, aby SRP dotyczyło każdego powodu ograniczenia zakresu. Jest więcej powodów do ograniczenia zakresu niż SRP. Ponadto ograniczam zakres, nalegając, aby klasa była abstrakcją, która może przyjąć nazwę zapewniającą, że zaglądanie do środka nie zaskoczy Cię .

Możesz zastosować Prawo Curly'ego do zajęć. Jesteś poza tym, o czym mówi wujek Bob, ale możesz to zrobić. Jeśli pomylisz się, zaczniesz myśleć, że oznacza to jedną funkcję. To tak, jakby myśleć, że rodzina powinna mieć tylko jedno dziecko. Posiadanie więcej niż jednego dziecka nie powstrzymuje go od bycia rodziną.

Jeśli zastosujesz prawo Curly'ego do klasy, wszystko w klasie powinno dotyczyć jednej idei jednoczącej. Ten pomysł może być szeroki. Chodzi o wytrwałość. Jeśli są tam jakieś funkcje rejestrowania, są one wyraźnie nie na miejscu. Nie ma znaczenia, czy pan X jest jedynym, który dba o ten kod.

Klasyczna zasada, którą należy tu zastosować, nazywa się oddzieleniem obaw . Jeśli oddzielisz wszystkie swoje obawy, można argumentować, że to, co zostało w jednym miejscu, jest jednym z nich. Tak nazwaliśmy ten pomysł, zanim film City Slickers z 1991 roku przedstawił nam postać Curly.

To jest w porządku. Po prostu to, co wujek Bob nazywa odpowiedzialnością, nie jest problemem. Na nim nie spoczywa odpowiedzialność. To coś, co może zmusić cię do zmiany. Możesz skupić się na jednym problemie i nadal tworzyć kod, który będzie odpowiedzialny przed różnymi grupami ludzi o różnych programach.

Może nie obchodzi cię to. W porządku. Myślenie, że trzymanie się „robienia jednej rzeczy” rozwiąże wszystkie wasze problemy projektowe, pokazuje brak wyobrażenia o tym, czym może być „jedna rzecz”. Innym powodem ograniczenia zakresu jest organizacja. Możesz zagnieździć wiele „jednej rzeczy” w innych „jednej rzeczy”, dopóki nie pojawi się szuflada śmieci pełna wszystkiego. Mówiłem o tym wcześniej

Oczywiście klasycznym powodem OOP do ograniczenia zakresu jest to, że klasa ma w nim prywatne pola, a zamiast tego używają getterów do dzielenia się tymi danymi, umieszczamy każdą metodę, która potrzebuje tych danych w klasie, w której mogą używać danych prywatnie. Wielu uważa, że ​​jest to zbyt restrykcyjne, aby używać go jako ogranicznika zakresu, ponieważ nie każda metoda, która należy do siebie, używa dokładnie tych samych pól. Chciałbym zapewnić, że jakikolwiek pomysł, który połączył dane, był tym samym pomysłem, który połączył metody.

Sposób funkcjonalny spojrzeć na to, że a.f(x)i a.g(x)po prostu f a (x) i g (x). Nie dwie funkcje, ale kontinuum par funkcji, które się różnią. Nie musi nawet zawierać danych. Może to być po prostu jak wiesz, który i realizacja masz zamiar użyć. Funkcje, które zmieniają się razem, należą do siebie. To dobry stary polimorfizm.afg

SRP jest tylko jednym z wielu powodów ograniczenia zakresu. Ten jest dobry. Ale nie jedyny.

candied_orange
źródło
25
Myślę, że ta odpowiedź jest myląca dla kogoś, kto próbuje znaleźć SRP. Bitwa między panem prezydentem a panią dyrektor nie została rozwiązana za pomocą środków technicznych, a wykorzystanie go do uzasadnienia decyzji inżynierskiej jest bezsensowne. Prawo Conwaya w akcji.
whatsisname
8
@whatsisname Wręcz przeciwnie. SRP miał wyraźnie dotyczyć interesariuszy. Nie ma to nic wspólnego z projektem technicznym. Możesz nie zgodzić się z tym podejściem, ale w taki właśnie sposób SRP został pierwotnie zdefiniowany przez wuja Boba i musiał to powtarzać w kółko, ponieważ z jakiegoś powodu ludzie nie są w stanie zrozumieć tego prostego pojęcia (umysł, czy to naprawdę przydatne jest całkowicie ortogonalne pytanie).
Luaan
Prawo Curly'ego, jak opisał Tim Ottinger, podkreśla, że ​​zmienna powinna konsekwentnie oznaczać jedno. Dla mnie SRP jest nieco silniejszy; klasa może koncepcyjnie reprezentować „jedną rzecz”, ale naruszać SRP, jeśli dwa zewnętrzne czynniki zmiany traktują jakiś aspekt tej „jednej rzeczy” na różne sposoby lub obawiają się o dwa różne aspekty. Problem polega na modelowaniu; wybrałeś modelowanie czegoś jako pojedynczej klasy, ale jest coś w domenie, która sprawia, że ​​ten wybór jest problematyczny (sprawy zaczynają ci przeszkadzać w miarę ewolucji bazy kodu).
Filip Milovanović
2
@ FilipMilovanović Podobieństwo, które widzę między prawem Conwaya a SRP, w jaki sposób wujek Bob wyjaśnił SRP w swojej książce o czystej architekturze, wynika z założenia, że ​​organizacja ma czystą acykliczną tabelę org. To jest stary pomysł. Nawet Biblia ma tutaj cytat: „Żaden człowiek nie może dwom panom służyć”.
candied_orange
1
@ TKK powiązuję to (nie zrównując) z prawem Conwaya, a nie prawem Curly'ego. Odrzucam pomysł, że SRP jest prawem Curly'ego głównie dlatego, że sam wujek Bob tak to powiedział w książce „Czysta architektura”.
candied_orange
48

Kluczem tutaj jest zakres lub, jeśli wolisz, szczegółowość . Część funkcjonalności reprezentowana przez klasę można dalej podzielić na części funkcjonalności, przy czym każda część jest metodą.

Oto przykład. Wyobraź sobie, że musisz utworzyć plik CSV z sekwencji. Jeśli chcesz być zgodny z RFC 4180, wdrożenie algorytmu i obsłużenie wszystkich przypadków brzegowych zajmie trochę czasu.

Wykonanie tego w jednej metodzie spowodowałoby kod, który nie będzie szczególnie czytelny, a zwłaszcza metoda zrobiłaby kilka rzeczy na raz. Dlatego podzielisz go na kilka metod; na przykład jeden z nich może być odpowiedzialny za generowanie nagłówka, tj. pierwszej linii CSV, podczas gdy inna metoda przekształciłaby wartość dowolnego typu na jego reprezentację ciągu odpowiednią dla formatu CSV, podczas gdy inna określiłaby, czy wartość należy ująć w podwójne cudzysłowy.

Metody te mają własną odpowiedzialność. Metoda, która sprawdza, czy istnieje potrzeba dodania podwójnych cudzysłowów, ma swoją własną, a metoda, która generuje nagłówek, ma jedną. Jest to SRP stosowane do metod .

Teraz wszystkie te metody mają jeden wspólny cel, to jest wykonanie sekwencji i wygenerowanie CSV. Jest to jedyna odpowiedzialność klasy .


Pablo H skomentował:

Dobry przykład, ale wydaje mi się, że wciąż nie odpowiada, dlaczego SRP pozwala klasie mieć więcej niż jedną metodę publiczną.

W rzeczy samej. Podany przeze mnie przykład CSV ma jedną metodę publiczną, a wszystkie pozostałe są prywatne. Lepszym przykładem może być kolejka zaimplementowana przez Queueklasę. Ta klasa zawierałaby w zasadzie dwie metody: push(również wywoływana enqueue) i pop(również wywoływana dequeue).

  • Obowiązkiem Queue.pushjest dodanie obiektu do ogona kolejki.

  • Obowiązkiem Queue.popjest usunięcie obiektu z głowy kolejki i obsłużenie przypadku, gdy kolejka jest pusta.

  • Obowiązkiem Queueklasy jest zapewnienie logiki kolejki.

Arseni Mourzenko
źródło
1
Dobry przykład, ale wydaje mi się, że wciąż nie odpowiada, dlaczego SRP pozwala klasie mieć więcej niż jedną metodę publiczną .
Pablo H
1
@PabloH: fair. Dodałem inny przykład, w którym klasa ma dwie metody.
Arseni Mourzenko
30

Funkcja jest funkcją.

Odpowiedzialność to odpowiedzialność.

Mechanik odpowiada za naprawę samochodów, co będzie wiązało się z diagnostyką, kilkoma prostymi zadaniami konserwacyjnymi, niektórymi faktycznymi naprawami, niektórymi przekazaniem zadań innym itp.

Klasa kontenera (lista, tablica, słownik, mapa itp.) Jest odpowiedzialna za przechowywanie obiektów, co obejmuje przechowywanie ich, umożliwianie wstawiania, zapewniania dostępu, pewnego rodzaju porządkowania itp.

Pojedyncza odpowiedzialność nie oznacza, że ​​jest bardzo mało kodu / funkcjonalności, oznacza to, że każda funkcjonalność „należy do siebie” pod tą samą odpowiedzialnością.

Piotr
źródło
2
Zgodzić się. @Aulis Ronkainen - powiązać dwie odpowiedzi. A w przypadku zagnieżdżonych obowiązków, korzystając z analogii mechanicznej, garaż odpowiada za utrzymanie pojazdów. różni mechanicy w garażu są odpowiedzialni za różne części samochodu, ale każda z tych mechaników działa razem w spójność
wolfsshield
2
@wolfsshield, zgodził się. Mechanik, który wykonuje tylko jedną rzecz, jest bezużyteczny, ale mechanik, który ponosi jedną odpowiedzialność, nie jest (przynajmniej niekoniecznie). Chociaż analogie z życia codziennego nie zawsze najlepiej opisują abstrakcyjne koncepcje OOP, ważne jest rozróżnienie tych różnic. Uważam, że niezrozumienie różnicy powoduje przede wszystkim zamieszanie.
Aulis Ronkainen
3
@AulisRonkainen Chociaż wygląda, pachnie i wydaje się być analogią, zamierzałem użyć tej mechaniki, aby podkreślić konkretne znaczenie terminu Odpowiedzialność w SRP. Całkowicie zgadzam się z twoją odpowiedzią.
Peter
20

Jedna odpowiedzialność niekoniecznie oznacza, że ​​robi tylko jedną rzecz.

Weźmy na przykład klasę usług użytkownika:

class UserService {
    public User Get(int id) { /* ... */ }
    public User[] List() { /* ... */ }

    public bool Create(User u) { /* ... */ }
    public bool Exists(int id) { /* ... */ }
    public bool Update(User u) { /* ... */ }
}

Ta klasa ma wiele metod, ale jej odpowiedzialność jest jasna. Zapewnia dostęp do rekordów użytkowników w magazynie danych. Jedyne zależności to model użytkownika i magazyn danych. Jest luźno sprzężony i bardzo spójny, a tak naprawdę SRP próbuje sprawić, byś pomyślał.

SRP nie należy mylić z „zasadą segregacji interfejsów” (patrz SOLID ). Zasada segregacji interfejsów (ISP) mówi, że mniejsze, lekkie interfejsy są lepsze niż większe, bardziej uogólnione interfejsy. Go intensywnie wykorzystuje ISP w swojej standardowej bibliotece:

// Interface to read bytes from a stream
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface to write bytes to a stream
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface to convert an object into JSON
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

SRP i ISP są z pewnością powiązane, ale jedno nie implikuje drugiego. ISP jest na poziomie interfejsu, a SRP na poziomie klasy. Jeśli klasa implementuje kilka prostych interfejsów, może nie mieć już tylko jednej odpowiedzialności.

Podziękowania dla Luaana za wskazanie różnicy między ISP a SRP.

Jesse
źródło
3
W rzeczywistości opisujesz zasadę segregacji interfejsu („I” w SOLID). SRP to zupełnie inna bestia.
Luaan
Poza tym, jakiej konwencji kodowania używasz tutaj? Spodziewam się do obiektów UserService i Userbyć UpperCamelCase, ale metody Create , Existsi Updatebym się lowerCamelCase.
KlaymenDK
1
@KlaymenDK Masz rację, wielkie litery to tylko nawyk korzystania z Go (wielkie litery = wyeksportowane / publiczne, małe = prywatne)
Jesse
@Luaan Dzięki za zwrócenie na to uwagi wyjaśnię moją odpowiedź
Jesse
1
@KlaymenDK Wiele języków używa PascalCase do metod i klas. Na przykład C #.
Omegastick
15

W restauracji jest szef kuchni. Jego jedynym obowiązkiem jest gotować. Ale potrafi gotować steki, ziemniaki, brokuły i setki innych rzeczy. Czy zatrudniłbyś jednego szefa kuchni na danie w swoim menu? A może jeden szef kuchni na każdy składnik każdego dania? Lub jeden szef kuchni, który może spełnić swoją jedyną odpowiedzialność: gotować?

Jeśli poprosisz szefa kuchni o wykonanie listy płac, wtedy naruszasz SRP.

gnasher729
źródło
4

Kontrprzykład: przechowywanie stanu zmiennego.

Załóżmy, że miałeś najprostszą klasę w historii, której jedynym zadaniem jest przechowywanie int.

public class State {
    private int i;


    public State(int i) { this.i = i; }
}

Jeśli jesteś ograniczony tylko do 1 metody, możesz mieć albo setState(), albo getState(), chyba że złamiesz enkapsulację i iupublicznisz.

  • Seter jest bezużyteczny bez gettera (nigdy nie można odczytać informacji)
  • Getter jest bezużyteczny bez setera (nigdy nie można mutować informacji).

Tak wyraźnie, ta pojedyncza odpowiedzialność wymaga posiadania co najmniej 2 metod w tej klasie. CO BYŁO DO OKAZANIA.

Alexander
źródło
4

Źle interpretujesz zasadę pojedynczej odpowiedzialności.

Pojedyncza odpowiedzialność nie oznacza jednej metody. Oznaczają różne rzeczy. W rozwoju oprogramowania mówimy o spójności . Funkcje (metody) charakteryzujące się wysoką spójnością „należą” do siebie i mogą być liczone jako wykonywanie pojedynczej odpowiedzialności.

Projektant systemu musi zaprojektować system w taki sposób, aby zasada pojedynczej odpowiedzialności była spełniona. Można to postrzegać jako technikę abstrakcji i dlatego czasami jest to kwestia opinii. Wdrożenie zasady pojedynczej odpowiedzialności sprawia, że ​​kod jest łatwiejszy do przetestowania i łatwiejszy do zrozumienia jego architektury i projektu.

Aulis Ronkainen
źródło
2

Często pomocne jest (w dowolnym języku, ale szczególnie w językach OO) patrzeć na rzeczy i organizować je z punktu widzenia danych, a nie funkcji.

Dlatego należy rozważyć odpowiedzialność klasy za utrzymanie integralności i zapewnienie pomocy w prawidłowym wykorzystaniu danych, które posiada. Oczywiście jest to łatwiejsze, jeśli cały kod jest w jednej klasie, niż rozłożony na kilka klas. Dodanie dwóch punktów jest bardziej niezawodne, a kod łatwiejszy w utrzymaniu, dzięki Point add(Point p)metodzie w Pointklasie niż w innym miejscu.

W szczególności klasa nie powinna ujawniać niczego, co mogłoby skutkować niespójnymi lub niepoprawnymi danymi. Na przykład, jeśli Pointmusi znajdować się w płaszczyźnie (0,0) do (127, 127), konstruktor i wszelkie metody, które modyfikują lub produkują nowy Pointmają obowiązek sprawdzania podanych wartości i odrzucania wszelkich zmian, które mogłyby to naruszać wymaganie. (Często coś takiego Pointbyłoby niezmienne, a upewnienie się, że nie ma żadnych sposobów modyfikacji Pointpo jego zbudowaniu, byłoby również odpowiedzialnością klasy)

Pamiętaj, że warstwowanie tutaj jest całkowicie dopuszczalne. Możesz mieć Pointklasę do radzenia sobie z poszczególnymi punktami i Polygonklasę do radzenia sobie z zestawem Points; nadal mają one osobne obowiązki, ponieważ Polygonprzekazują całą odpowiedzialność za radzenie sobie z czymkolwiek wyłącznie związanym z Point(na przykład za zapewnienie, że punkt ma zarówno wartość, jak xi ywartość) Pointklasie.

Curt J. Sampson
źródło