Po prostu jakich praktycznych technik używają ludzie, aby sprawdzić, czy klasa narusza zasadę pojedynczej odpowiedzialności?
Wiem, że klasa powinna mieć tylko jeden powód do zmiany, ale w tym zdaniu brakuje praktycznego sposobu, aby to naprawdę wdrożyć.
Jedynym sposobem, jaki znalazłem, jest użycie zdania „The ......... powinien ......... sam w sobie”. gdzie pierwsza spacja to nazwa klasy, a później nazwa metody (odpowiedzialności).
Czasami jednak trudno jest ustalić, czy odpowiedzialność rzeczywiście narusza SRP.
Czy jest więcej sposobów na sprawdzenie SRP?
Uwaga:
Pytanie nie dotyczy tego, co oznacza SRP, ale praktyczną metodologię lub szereg kroków sprawdzających i wdrażających SRP.
AKTUALIZACJA
Dodałem przykładową klasę, która wyraźnie narusza SRP. Byłoby wspaniale, gdyby ludzie mogli użyć go jako przykładu do wyjaśnienia, w jaki sposób podchodzą do zasady pojedynczej odpowiedzialności.
Przykład pochodzi stąd .
Odpowiedzi:
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 interfejsuprintReport
. 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 jakIPrintable<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 zaletyReport
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 wIncomeStatement
(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 formiegetTotalRevenues()
,getTotalExpenses()
orazgetNetIncome()
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
iprint
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.
źródło
IncomeStatement
. Czy proponowany projekt oznacza, żeIncomeStatement
będą miały instancjeIncomeStatementPrinter
iIncomeStatementRenderer
tak, że gdy zadzwonięprint()
naIncomeStatement
nim będzie przekazywać wywołanieIncomeStatementPrinter
zamiast?IncomeStatement
klasa nie jestprint
metoda, aniformat
metody, ani żadnej innej metody, która nie jest bezpośrednio czynienia z inspekcji lub manipulowania danymi raportu siebie. Po to są te inne klasy. Jeśli chcesz wydrukować jeden, bierzesz zależność odIPrintable<IncomeStatement>
interfejsu zarejestrowanego w kontenerze.Printer
instancję wIncomeStatement
klasie? wyobrażam sobie, jak to się stanie, kiedyIncomeStatement.print()
go zadzwonię , przekaże goIncomeStatementPrinter.print(this, format)
. Co jest złego w tym podejściu? ... Kolejne pytanie, o którym wspomniałeś,IncomeStatement
powinno zawierać informacje, które pojawiają się w sformatowanych raportach, jeśli chcę, aby były one odczytywane z bazy danych lub pliku XML, czy powinienem wyodrębnić metodę, która ładuje dane do osobnej klasy i przekazać do niej połączenieIncomeStatement
?IncomeStatementPrinter
zależneIncomeStatement
iIncomeStatement
zależne odIncomeStatementPrinter
. To cykliczna zależność. I to po prostu zły projekt; nie ma żadnego powodu,IncomeStatement
aby wiedzieć coś oPrinter
lubIncomeStatementPrinter
- jest to model domeny, nie zajmuje się drukowaniem, a delegacja jest bezcelowa, ponieważ każda inna klasa może utworzyć lub uzyskaćIncomeStatementPrinter
. Nie ma dobrego powodu, aby mieć pojęcie o drukowaniu w modelu domeny.IncomeStatement
bazy danych (lub pliku XML) - zwykle jest to obsługiwane przez repozytorium i / lub program odwzorowujący, a nie domenę, i ponownie nie delegujesz tego w domenie; jeśli jakaś inna klasa musi odczytać jeden z tych modeli, wówczas wyraźnie prosi o to repozytorium . Chyba że wdrażasz wzorzec Active Record, ale chyba nie jestem fanem.Sprawdzam SRP, sprawdzając każdą metodę (odpowiedzialność) klasy i zadając następujące pytanie:
„Czy kiedykolwiek będę musiał zmienić sposób wdrażania tej funkcji?”
Jeśli znajdę funkcję, którą będę musiał zaimplementować na różne sposoby (w zależności od jakiejś konfiguracji lub warunków), to wiem na pewno, że potrzebuję dodatkowej klasy do obsługi tej odpowiedzialności.
źródło
Oto cytat z reguły 8 Object Calisthenics :
Biorąc pod uwagę ten (nieco idealistyczny) pogląd, można powiedzieć, że każda klasa, która zawiera tylko jedną lub dwie zmienne stanu, prawdopodobnie nie naruszy SRP. Można również powiedzieć, że każda klasa zawierająca więcej niż dwie zmienne stanu może naruszać SRP.
źródło
Jedna możliwa implementacja (w Javie). Korzystałem ze swobód z typami zwrotu, ale wydaje mi się, że to odpowiada na pytanie. TBH Nie sądzę, że interfejs do klasy Report jest taki zły, chociaż lepsza nazwa może być w porządku. Pominąłem oświadczenia i twierdzenia o zwięzłości.
EDYCJA: Zauważ też, że klasa jest niezmienna. Po utworzeniu nie możesz nic zmienić. Możesz dodać setFormatter () i setPrinter () i nie wpakować się w zbyt duże kłopoty. Kluczem jest IMHO, aby nie zmieniać surowych danych po utworzeniu instancji.
źródło
if (reportData == null)
zakładam, że masz na myślidata
. Po drugie, miałem nadzieję dowiedzieć się, jak doszło do tej implementacji. Na przykład dlaczego zdecydowałeś się zamiast tego przekazać wszystkie połączenia innym obiektom. Jeszcze jedna rzecz, nad którą zawsze się zastanawiałem: czy to naprawdę odpowiedzialność za wydrukowanie raportu ?! Dlaczego nie stworzyłeś osobnejprinter
klasy, która przyjmujereport
konstruktor?Printer
Klasa, która bierze raport lubReport
klasę, że trwa drukarkę? Zetknąłem się z podobnym problemem wcześniej, gdy musiałem parsować raport i spierałem się z moją TL, czy powinniśmy zbudować analizator składni, który pobiera raport, czy też raport powinien zawierać analizator składni iparse()
połączenie jest do niego delegowane.W twoim przykładzie nie jest jasne, czy SRP jest naruszane. Może raport powinien być w stanie sam sformatować i wydrukować, jeśli są one stosunkowo proste:
Metody są tak proste, że nie ma sensu mieć
ReportFormatter
aniReportPrinter
klas. Jedynym rażącym problemem w interfejsie jestgetReportData
to, że narusza ono pytanie „nie mów” na obiekcie niebędącym wartością.Z drugiej strony, jeśli metody są bardzo skomplikowane lub istnieje wiele sposobów formatowania lub drukowania,
Report
wówczas sensowne jest przekazanie odpowiedzialności (również bardziej testowalnej):SRP jest zasadą projektowania, a nie filozoficzną koncepcją, dlatego opiera się na rzeczywistym kodzie, z którym pracujesz. Semantycznie możesz podzielić lub pogrupować klasę na tyle obowiązków, ile chcesz. Jednak, jako praktyczna zasada, SRP powinien pomóc ci znaleźć kod, który musisz zmodyfikować . Znaki, które naruszasz SRP to:
Możesz to naprawić poprzez refaktoryzację poprzez ulepszanie nazw, grupowanie podobnego kodu, eliminowanie powielania, stosowanie projektowania warstwowego i dzielenie / łączenie klas w razie potrzeby. Najlepszym sposobem na naukę SRP jest zanurzenie się w bazie kodu i refaktoryzacja bólu.
źródło
Printer
Klasa, która bierze raport lubReport
klasę, że trwa drukarkę? Wiele razy mam do czynienia z takim pytaniem projektowym, zanim zastanawiam się, czy kod okaże się złożony, czy nie.Zasada jednolitej odpowiedzialności jest ściśle powiązana z pojęciem spójności . Aby mieć bardzo spójną klasę, musisz mieć współzależność między zmiennymi instancji klasy i jej metodami; to znaczy każda z metod powinna manipulować jak największą liczbą zmiennych instancji. Im więcej zmiennych używa metoda, tym bardziej spójna jest jej klasa; maksymalna spójność jest zwykle nieosiągalna.
Ponadto, aby dobrze zastosować SRP, dobrze rozumiesz domenę logiki biznesowej; wiedzieć, co powinna robić każda abstrakcja. Architektura warstwowa jest również powiązana z SRP, ponieważ każda warstwa wykonuje określoną czynność (warstwa źródła danych powinna zapewniać dane itd.).
Wracając do spójności, nawet jeśli twoje metody nie wykorzystują wszystkich zmiennych, należy je połączyć:
Nie powinieneś mieć czegoś takiego jak poniższy kod, w którym część zmiennych instancji jest używana w części metod, a druga część zmiennych jest używana w innej części metod (tutaj powinieneś mieć dwie klasy dla każda część zmiennych).
źródło