Jakie są praktyczne sposoby wdrożenia SRP?

11

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

Zgłoś klasę

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 .

Songo
źródło
To interesująca zasada, ale nadal można napisać: „Klasa osoby może się zrenderować”. Można to uznać za naruszenie SRP, ponieważ włączenie GUI do tej samej klasy, która zawiera reguły biznesowe i utrwalanie danych, nie jest w porządku. Więc myślę, że trzeba dodać koncepcji architektonicznych domen (warstw i warstw) i upewnij się, że stwierdzenie to jest ważne z 1 tych domenie tylko (takich jak GUI, Data Access, itp)
NoChance
@EmmadKareem Ta zasada została wspomniana w pierwszej analizie obiektowej i projektowaniu, i właśnie o tym myślałem. Trochę brakuje praktycznego sposobu na jego wdrożenie. Wspomnieli, że czasami obowiązki nie będą tak oczywiste dla projektanta i musi on kierować się zdrowym rozsądkiem, aby ocenić, czy metoda powinna naprawdę należeć do tej klasy, czy nie.
Songo
Jeśli naprawdę chcesz zrozumieć SRP, przeczytaj niektóre pisma wuja Boba Martina. Jego kod jest jednym z najładniejszych, jakie widziałem, i ufam, że to, co mówi o SRP, to nie tylko dobra rada, ale także coś więcej niż tylko machanie ręką.
Robert Harvey
I czy głosujący na niższy głos wyjaśniłby, dlaczego warto poprawić post ?!
Songo

Odpowiedzi:

7

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 Reportstosowane 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 Reportklasy.

  • 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ą.

  • formatReportto 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 IncomeStatementwstecz 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 IncomeStatementPrinterklasie, może używać, IPrintable<T>a tym samym działać na dowolnym raporcie drukowanym, co daje wszystkie postrzegane zalety Reportklasy bazowej za pomocą printmetody 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 IncomeStatementto 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 IncomeStatementklasa 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 formati printmetody, 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.

Aaronaught
źródło
+1 świetna odpowiedź. Jestem jednak zdezorientowany co do klasy IncomeStatement. Czy proponowany projekt oznacza, że IncomeStatementbędą miały instancje IncomeStatementPrinteri IncomeStatementRenderertak, że gdy zadzwonię print()na IncomeStatementnim będzie przekazywać wywołanie IncomeStatementPrinterzamiast?
Songo,
@Songo: Absolutnie nie! Nie powinieneś mieć cyklicznych zależności, jeśli korzystasz z SOLID. Widocznie moja odpowiedź nie na tyle jasno, że IncomeStatementklasa nie jest printmetoda, ani formatmetody, 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ść od IPrintable<IncomeStatement>interfejsu zarejestrowanego w kontenerze.
Aaronaught,
aah rozumiem twój punkt widzenia. Gdzie jednak jest zależność cykliczna, jeśli wstrzyknę Printerinstancję w IncomeStatementklasie? wyobrażam sobie, jak to się stanie, kiedy IncomeStatement.print()go zadzwonię , przekaże go IncomeStatementPrinter.print(this, format). Co jest złego w tym podejściu? ... Kolejne pytanie, o którym wspomniałeś, IncomeStatementpowinno 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łączenie IncomeStatement?
Songo,
@Songo: Masz IncomeStatementPrinterzależne IncomeStatementi IncomeStatementzależne od IncomeStatementPrinter. To cykliczna zależność. I to po prostu zły projekt; nie ma żadnego powodu, IncomeStatementaby wiedzieć coś o Printerlub IncomeStatementPrinter- 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.
Aaronaught,
Jeśli chodzi o sposób ładowania IncomeStatementbazy 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.
Aaronaught,
2

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.

John Raya
źródło
1

Oto cytat z reguły 8 Object Calisthenics :

Większość klas powinna po prostu odpowiadać za obsługę jednej zmiennej stanu, ale jest kilka, które będą wymagać dwóch. Dodanie nowej zmiennej instancji do klasy natychmiast zmniejsza spójność tej klasy. Zasadniczo podczas programowania na podstawie tych reguł można zauważyć, że istnieją dwa rodzaje klas, które utrzymują stan zmiennej pojedynczej instancji oraz te, które koordynują dwie oddzielne zmienne. Zasadniczo nie mieszaj dwóch rodzajów obowiązków

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.

MattDavey
źródło
2
Ten pogląd jest beznadziejnie uproszczony. Nawet słynne, ale proste równanie Einsteina wymaga dwóch zmiennych.
Robert Harvey
Pytanie PO było następujące: „Czy jest więcej sposobów na sprawdzenie SRP?” - jest to jeden z możliwych wskaźników. Tak, to jest uproszczone i nie wytrzymuje w każdym przypadku, ale jest to jeden ze sposobów sprawdzenia, czy SRP został naruszony.
MattDavey,
1
Podejrzewam, że stan zmienny vs niezmienny jest również ważnym czynnikiem
jk.
Reguła 8 opisuje idealny proces tworzenia projektów, które mają tysiące klas, co sprawia, że ​​system jest beznadziejnie złożony, niezrozumiały i niemożliwy do utrzymania. Ale zaletą jest to, że możesz śledzić SRP.
Dunk
@Dunk Nie zgadzam się z tobą, ale ta dyskusja jest całkowicie nie na temat pytania.
MattDavey,
1

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.

public class Report
{
    private ReportData data;
    private ReportDataDao dao;
    private ReportFormatter formatter;
    private ReportPrinter printer;


    /*
     *  Parameterized constructor for depndency injection, 
     *  there are better ways but this is explicit.
     */
    public Report(ReportDataDao dao, 
        ReportFormatter formatter, ReportPrinter printer)
    {
        super();
        this.dao = dao;
        this.formatter = formatter;
        this.printer = printer;
    }

    /*
     * Delegates to the injected printer.
     */
    public void printReport()
    {
        printer.print(formatReport());
    }


    /*
     * Lazy loading of data, delegates to the dao 
     * for the meat of the call.
     */
    public ReportData getReportData()
    {
        if (reportData == null)
        {
            reportData = dao.loadData();
        }
        return reportData;
    }

    /*
     * Delegate to the formatter for formatting 
     * (notice a pattern here).
     */
    public ReportData formatReport()
    {
        formatter.format(getReportData());
    }
}
Heath Lilley
źródło
Dzięki za wdrożenie. Mam 2 rzeczy, w linii if (reportData == null)zakładam, że masz na myśli data. 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ś osobnej printerklasy, która przyjmuje reportkonstruktor?
Songo,
Tak, reportData = dane, przepraszam za to. Delegowanie umożliwia precyzyjną kontrolę zależności. W czasie wykonywania możesz zapewnić alternatywne implementacje dla każdego komponentu. Teraz możesz mieć HtmlPrinter, PdfPrinter, JsonPrinter, ... itd. Jest to również przydatne do testowania, ponieważ możesz przetestować delegowane komponenty w izolacji, jak również zintegrować z powyższym obiektem. Z pewnością można odwrócić relację między drukarką a raportem, chciałem tylko pokazać, że możliwe jest zapewnienie rozwiązania z zapewnionym interfejsem klasy. Jest to nawyk pracy nad starszymi systemami. :)
Heath Lilley,
hmmmm ... Więc jeśli budowałeś system od zera, którą opcję byś wybrał? PrinterKlasa, która bierze raport lub Reportklasę, ż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 i parse()połączenie jest do niego delegowane.
Songo,
Zrobiłbym zarówno ... print.print (raport), aby rozpocząć i report.print (), jeśli zajdzie taka potrzeba później. Wspaniałą rzeczą w podejściu do print.print (raportu) jest to, że można go wielokrotnie używać. Oddziela odpowiedzialność i umożliwia korzystanie z wygodnych metod tam, gdzie są potrzebne. Być może nie chcesz, aby inne obiekty w twoim systemie wiedziały o ReportPrinter, więc dzięki metodzie print () w klasie osiągasz poziom abstakcji, który izoluje logikę drukowania raportów od świata zewnętrznego. Wciąż ma wąski wektor zmian i jest łatwy w użyciu.
Heath Lilley,
0

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:

class Report {
  void format() {
     text = text.trim();
  }

  void print() {
     new Printer().write(text);
  }
}

Metody są tak proste, że nie ma sensu mieć ReportFormatterani ReportPrinterklas. Jedynym rażącym problemem w interfejsie jest getReportDatato, ż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, Reportwówczas sensowne jest przekazanie odpowiedzialności (również bardziej testowalnej):

class Report {
  void format(ReportFormatter formatter) {
     text = formatter.format(text);
  }

  void print(ReportPrinter printer) {
     printer.write(text);
  }
}

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:

  • Zajęcia są tak duże, że tracisz czas na przewijanie lub szukanie właściwej metody.
  • Zajęcia są tak małe i liczne, że tracisz czas na przeskakiwanie między nimi lub znajdowanie właściwego.
  • Kiedy trzeba wprowadzić zmiany, wpływa to na tak wiele klas, że trudno jest je śledzić.
  • Kiedy trzeba dokonać zmiany, nie jest jasne, jakie klasy należy zmienić.

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.

Garrett Hall
źródło
czy możesz sprawdzić przykład, który załączyłem do postu i na tej podstawie opracować twoją odpowiedź?
Songo,
Zaktualizowano SRP zależy od kontekstu, jeśli opublikowałeś całą klasę (w osobnym pytaniu), łatwiej byłoby to wyjaśnić.
Garrett Hall,
Dziękuję za aktualizację. Pytanie jednak: czy to naprawdę odpowiedzialność za wydrukowanie raportu ?! Dlaczego nie stworzyłeś osobnej klasy drukarek, która pobiera raport w swoim konstruktorze?
Songo,
Mówię tylko, że SRP zależy od samego kodu, którego nie należy stosować dogmatycznie.
Garrett Hall,
tak, rozumiem o co ci chodzi. Ale jeśli budujesz system od zera, którą opcję byś wybrał? PrinterKlasa, która bierze raport lub Reportklasę, ż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.
Songo,
0

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ć:

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public Type1 method3() {
        //use var2 and var3
    }
}

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).

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;
    private TypeA varA;
    private TypeB varB;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public TypeA methodA() {
        //use varA and varB
    }

    public TypeA methodB() {
        //use varA
    }
}
m3th0dman
źródło