SRP stwierdza, bez żadnych wątpliwości, że klasa powinna mieć tylko jeden powód do zmiany.
Dekonstruując klasę „report” w pytaniu, ma ona trzy metody:
printReport
getReportData
formatReport
Ignorując nadmiarowe Report
stosowane w każdej metodzie, łatwo jest zrozumieć, dlaczego narusza to SRP:
Termin „drukuj” oznacza jakiś interfejs użytkownika lub rzeczywistą drukarkę. Dlatego klasa ta zawiera pewną ilość interfejsu użytkownika lub logiki prezentacji. Zmiana wymagań interfejsu użytkownika będzie wymagać zmiany Report
klasy.
Termin „dane” implikuje jakąś strukturę danych, ale tak naprawdę nie określa, co (XML? JSON? CSV?). Niezależnie od tego, czy „treść” raportu kiedykolwiek się zmieni, to również ta metoda. Istnieje sprzężenie z bazą danych lub domeną.
formatReport
to po prostu okropna nazwa metody, ale przypuszczam, że po raz kolejny ma to coś wspólnego z interfejsem użytkownika i prawdopodobnie inny aspekt interfejsu printReport
. Kolejny niezwiązany powód do zmiany.
Tak więc ta jedna klasa jest prawdopodobnie sprzężona z bazą danych, ekranem / urządzeniem drukującym i pewną wewnętrzną logiką formatowania dzienników lub danych wyjściowych plików. Mając wszystkie trzy funkcje w jednej klasie, zwielokrotniasz liczbę zależności i potrajasz prawdopodobieństwo, że jakakolwiek zmiana zależności lub wymagania spowoduje uszkodzenie tej klasy (lub czegoś innego, co od niej zależy).
Problem polega na tym, że wybrałeś szczególnie ciernisty przykład. Prawdopodobnie nie powinieneś mieć klasy o nazwie Report
, nawet jeśli robi to tylko jedna rzecz , ponieważ ... jaki raport? Czy wszystkie „raporty” nie są zupełnie różnymi bestiami, opartymi na różnych danych i różnych wymaganiach? I czy raport nie jest już sformatowany ani na ekranie, ani na wydruku?
Ale patrząc IncomeStatement
wstecz i tworząc hipotetyczną konkretną nazwę - nazwijmy to (jeden bardzo częsty raport) - właściwa architektura „SRPed” miałaby trzy typy:
IncomeStatement
- domena i / lub klasa modelu, która zawiera i / lub oblicza informacje pojawiające się w sformatowanych raportach.
IncomeStatementPrinter
, który prawdopodobnie zaimplementuje jakiś standardowy interfejs, taki jak IPrintable<T>
. Ma jedną kluczową metodę, Print(IncomeStatement)
a może kilka innych metod lub właściwości do konfiguracji ustawień drukowania.
IncomeStatementRenderer
, który obsługuje renderowanie ekranu i jest bardzo podobny do klasy drukarek.
Możesz także w końcu dodać więcej klas specyficznych dla funkcji, takich jak IncomeStatementExporter
/ IExportable<TReport, TFormat>
.
Jest to znacznie łatwiejsze w nowoczesnych językach dzięki wprowadzeniu generycznych i kontenerów IoC. Większość kodu aplikacji nie musi polegać na konkretnej IncomeStatementPrinter
klasie, może używać, IPrintable<T>
a tym samym działać na dowolnym raporcie drukowanym, co daje wszystkie postrzegane zalety Report
klasy bazowej za pomocą print
metody i nie powoduje zwykłych naruszeń SRP . Rzeczywistą implementację należy zadeklarować tylko raz, w rejestracji kontenera IoC.
Niektóre osoby, w konfrontacji z powyższym projektem, odpowiadają czymś w rodzaju: „ale wygląda to na kod proceduralny, a celem OOP było oderwanie nas od oddzielenia danych i zachowania!” Na co mówię: źle .
Nie IncomeStatement
są to tylko „dane”, a wspomniany błąd powoduje, że wielu ludzi z OOP ma wrażenie, że robią coś złego, tworząc taką „przezroczystą” klasę, a następnie zaczynają zagłuszać wszystkie niepowiązane funkcje w IncomeStatement
(no cóż, że i ogólne lenistwo). Ta klasa może początkowo być tylko danymi, ale z czasem jest gwarantowana, że stanie się bardziej modelem .
Na przykład rachunek zysków i strat zawiera całkowite przychody , wydatki ogółem i linie dochodów netto . Właściwie zaprojektowany system finansowy najprawdopodobniej nie będzie ich przechowywać, ponieważ nie są one danymi transakcyjnymi - w rzeczywistości zmieniają się w zależności od dodania nowych danych transakcyjnych. Jednak obliczenia tych wierszy zawsze będą dokładnie takie same, bez względu na to, czy drukujesz, renderujesz, czy eksportujesz raport. Więc twoja IncomeStatement
klasa będzie mieć wartość godziwą zachowania jej w formie getTotalRevenues()
, getTotalExpenses()
oraz getNetIncome()
metod i pewnie kilka innych. Jest to prawdziwy obiekt w stylu OOP z własnym zachowaniem, nawet jeśli tak naprawdę nie wydaje się „robić” zbyt wiele.
Ale format
i print
metody, one nie mają nic wspólnego z samą informacji. W rzeczywistości nie jest zbyt prawdopodobne, że będziesz chciał mieć kilka wdrożeń tych metod, np. Szczegółowe oświadczenie dla kierownictwa i niezbyt szczegółowe oświadczenie dla akcjonariuszy. Rozdzielenie tych niezależnych funkcji na różne klasy daje możliwość wyboru różnych implementacji w środowisku wykonawczym bez obciążania jedną uniwersalną print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
metodą. Fuj!
Mamy nadzieję, że zobaczysz, gdzie powyższa, masowo sparametryzowana metoda idzie źle i gdzie poszczególne implementacje idą dobrze; w przypadku pojedynczego obiektu za każdym razem, gdy dodajesz nowe zmarszczki do logiki drukowania, musisz zmienić model domeny ( Tim w finansach chce numerów stron, ale tylko w raporcie wewnętrznym, czy możesz to dodać? ), w przeciwieństwie do po prostu dodając właściwość konfiguracji do jednej lub dwóch klas satelitarnych.
Prawidłowe wdrożenie SRP polega na zarządzaniu zależnościami . W skrócie, jeśli klasa już robi coś pożytecznego i rozważasz dodanie innej metody, która wprowadziłaby nową zależność (taką jak interfejs użytkownika, drukarka, sieć, plik itp.), Nie . Zastanów się, jak możesz zamiast tego dodać tę funkcję do nowej klasy i jak dopasować tę nową klasę do ogólnej architektury (jest to dość łatwe, gdy projektujesz wokół wstrzykiwania zależności). To jest ogólna zasada / proces.
Uwaga dodatkowa: Podobnie jak Robert, zdecydowanie odrzucam pogląd, że klasa zgodna z SRP powinna mieć tylko jedną lub dwie zmienne stanu. Tak cienkiego opakowania rzadko można oczekiwać, że zrobi coś naprawdę przydatnego. Więc nie przesadzaj z tym.