Zasada pojedynczej odpowiedzialności - jak uniknąć fragmentacji kodu?

56

Pracuję w zespole, w którym kierownik zespołu jest zjadliwym orędownikiem zasad rozwoju SOLID. Brakuje mu jednak dużego doświadczenia w uzyskiwaniu skomplikowanego oprogramowania.

Mamy sytuację, w której zastosował SRP do tego, co było już dość złożoną bazą kodu, która stała się teraz bardzo rozdrobniona, trudna do zrozumienia i debugowania.

Mamy teraz problem nie tylko z fragmentacją kodu, ale także z enkapsulacją, ponieważ metody w klasie, która mogła być prywatna lub chroniona, zostały uznane za „powód do zmiany” i zostały wyodrębnione do publicznych lub wewnętrznych klas i interfejsów, które nie jest zgodny z celami enkapsulacji aplikacji.

Mamy konstruktorów klas, które przejmują 20 parametrów interfejsu, więc nasza rejestracja IoC staje się potworem samym w sobie.

Chcę wiedzieć, czy istnieje jakaś metoda „refaktoryzacji z dala od SRP”, której moglibyśmy użyć, aby rozwiązać niektóre z tych problemów. Czytałem, że nie narusza SOLID, jeśli utworzę wiele pustych, gruboziarnistych klas, które owijają wiele ściśle powiązanych klas, aby zapewnić pojedynczy punkt dostępu do sumy ich funkcjonalności (tj. Naśladować mniej nadmierna implementacja klasy SRP'd).

Poza tym nie mogę wymyślić rozwiązania, które pozwoli nam pragmatycznie kontynuować nasze wysiłki rozwojowe, jednocześnie ciesząc wszystkich.

Jakieś sugestie ?

Dean Chalk
źródło
18
To tylko moja opinia, ale myślę, że jest jeszcze jedna zasada, którą bardzo łatwo zapomnieć pod stosem różnych akronimów - „Zasada zdrowego rozsądku”. Kiedy „rozwiązanie” stwarza więcej problemów, które naprawdę rozwiązuje, coś jest nie tak. Uważam, że jeśli problem jest złożony, ale jest zamknięty w klasie, która dba o jego zawiłości i wciąż jest stosunkowo łatwa do debugowania - zostawiam go w spokoju. Zasadniczo wydaje mi się, że Twój pomysł na opakowanie jest odpowiedni, ale odpowiedź zostawię komuś bardziej kompetentnemu.
Patryk Ćwiek
6
Jeśli chodzi o „powód do zmiany” - nie trzeba przedwcześnie spekulować wszystkimi przyczynami. Poczekaj, aż będziesz musiał to zmienić, a następnie sprawdź, co można zrobić, aby taka zmiana była łatwiejsza w przyszłości.
62
Klasa z 20 parametrami konstruktora nie brzmi dla mnie bardzo SRP!
MattDavey
1
Piszesz „... rejestracja IoC i rozwiązanie ...”; brzmi to tak, jakbyś (lub lider zespołu) uważał, że „IoC” i „wstrzykiwanie zależności” (DI) to to samo, co nie jest prawdą. DI jest sposobem na osiągnięcie IoC, ale na pewno nie jedynym. Powinieneś dokładnie przeanalizować, dlaczego chcesz robić IoC; jeśli dzieje się tak, ponieważ chcesz napisać testy jednostkowe, możesz także spróbować użyć wzorca lokalizatora usług lub po prostu klas interfejsów ( ISomething). IMHO, te approches są znacznie łatwiejsze w obsłudze niż wstrzykiwanie zależności i dają bardziej czytelny kod.
2
każda udzielona tutaj odpowiedź byłaby próżnią; musielibyśmy zobaczyć kod, aby dać konkretną odpowiedź. 20 parametrów w konstruktorze? cóż, może brakować obiektu ... lub wszystkie mogą być poprawne; lub mogą należeć do pliku konfiguracyjnego, lub mogą należeć do klasy DI, lub ... Objawy z pewnością brzmią podejrzanie, ale jak większość rzeczy w CS, „to zależy” ...
Steven A. Lowe

Odpowiedzi:

84

Jeśli twoja klasa ma 20 parametrów w konstruktorze, nie wygląda na to, że twój zespół całkiem wie, czym jest SRP. Jeśli masz klasę, która robi tylko jedną rzecz, w jaki sposób ma 20 zależności? To tak, jakby wybrać się na wyprawę wędkarską i zabrać ze sobą wędkę, sprzęt, pikowanie, kulę do kręgli, nunchucks, miotacz ognia itp. Jeśli potrzebujesz wszystkiego, aby łowić ryby, nie tylko łowisz ryby.

To powiedziawszy, SRP, jak większość dostępnych tam zasad, może być nadmiernie stosowane. Jeśli stworzysz nową klasę do zwiększania liczb całkowitych, to tak, może to być jedna odpowiedzialność, ale daj spokój. To niedorzeczne. Zazwyczaj zapominamy, że takie rzeczy, jak zasady SOLID są po to, aby mieć jakiś cel. SOLID jest środkiem do celu, a nie celem samym w sobie. Celem jest łatwość konserwacji . Jeśli zamierzasz uzyskać tę szczegółowość dzięki zasadzie pojedynczej odpowiedzialności, jest to wskaźnik, że zapał dla SOLID oślepił zespół do osiągnięcia celu SOLID.

Więc myślę, że mówię ... SRP nie jest twoim problemem. To albo nieporozumienie z SRP, albo jego niezwykle szczegółowe zastosowanie. Postaraj się, aby twój zespół zachował najważniejsze. A najważniejsze jest łatwość konserwacji.

EDYTOWAĆ

Niech ludzie zaprojektują moduły w sposób zachęcający do łatwości użytkowania. Pomyśl o każdej klasie jak o mini API. Pomyśl najpierw: „Jak chciałbym skorzystać z tej klasy”, a następnie zaimplementuj ją. Nie myśl tylko: „Co ta klasa musi zrobić”. SRP ma wielką tendencję do utrudniania korzystania z klas, jeśli nie zastanawiasz się nad użytecznością.

EDYCJA 2

Jeśli szukasz wskazówek na temat refaktoryzacji, możesz zacząć robić to, co zasugerowałeś - utwórz grubsze klasy, aby zawinąć kilka innych. Upewnij się, że gruboziarnista klasa nadal przestrzega SRP , ale na wyższym poziomie. Masz dwie możliwości:

  1. Jeśli klasy drobnoziarniste nie są już używane w innym miejscu w systemie, możesz stopniowo przeciągać ich implementację do bardziej gruboziarnistej klasy i usuwać je.
  2. Zostaw klasy drobnoziarniste w spokoju. Być może zostały one dobrze zaprojektowane i potrzebujesz tylko opakowania, aby ułatwić ich użycie. Podejrzewam, że tak jest w przypadku większości twojego projektu.

Po zakończeniu refaktoryzacji (ale przed przystąpieniem do repozytorium) przejrzyj swoją pracę i zadaj sobie pytanie, czy refaktoryzacja rzeczywiście poprawiła łatwość konserwacji i łatwość użycia.

Phil
źródło
2
Alternatywny sposób na zachęcenie ludzi do myślenia o projektowaniu klas: pozwól im pisać karty CRC (nazwa klasy, odpowiedzialność, współpracownicy) . Jeśli klasa ma zbyt wielu współpracowników lub obowiązków, najprawdopodobniej nie wystarczy SRP. Innymi słowy, cały tekst musi zmieścić się na karcie indeksu, w przeciwnym razie robi zbyt wiele.
Spoike
18
Wiem do czego służy miotacz ognia, ale jak, u licha, łowisz na słupie?
R. Martinho Fernandes
13
+1 SOLID jest środkiem do celu, a nie celem samym w sobie.
B, 7
1
+1: Kłóciłem się wcześniej, że rzeczy takie jak „Prawo Demetera” są źle nazwane, powinna to być „Linia przewodnika Demetera”. Te rzeczy powinny działać dla ciebie, nie powinieneś pracować dla nich.
Binary Worrier
2
@EmmadKareem: To prawda, że ​​obiekty DAO powinny mieć kilka właściwości. Ale z drugiej strony istnieje kilka rzeczy, które możesz pogrupować w coś tak prostego jak Customerklasa i mieć łatwiejszy do utrzymania kod. Zobacz przykłady tutaj: codemonkeyism.com/…
Spoike
32

Myślę, że to właśnie w Refaktoryzacji Martina Fowlera przeczytałem kiedyś SRP, określając, gdzie idzie za daleko. Jest drugie pytanie, równie ważne jak „czy każda klasa ma tylko jeden powód do zmiany?” i to znaczy „czy każda zmiana dotyczy tylko jednej klasy?”

Jeśli odpowiedź na pierwsze pytanie brzmi „tak”, ale drugie pytanie „nie jest nawet bliskie”, musisz ponownie spojrzeć na to, jak wdrażasz SRP.

Na przykład, jeśli dodanie jednego pola do tabeli oznacza, że ​​musisz zmienić DTO i klasę sprawdzania poprawności oraz klasę trwałości i obiekt modelu widoku itd., Oznacza to, że masz problem. Może powinieneś przemyśleć, jak wdrożyłeś SRP.

Być może powiedziałeś, że dodanie pola jest powodem zmiany obiektu klienta, ale zmiana warstwy trwałości (powiedzmy z pliku XML na bazę danych) to kolejny powód do zmiany obiektu klienta. Więc decydujesz się również utworzyć obiekt CustomerPersistence. Ale jeśli zrobisz to tak, że dodanie pola STILL wymaga zmiany w obiekcie CustomerPersisitence, to jaki był sens? Nadal masz obiekt z dwoma przyczynami do zmiany - to po prostu już nie Klient.

Jeśli jednak wprowadzisz ORM, jest całkiem możliwe, że możesz sprawić, aby klasy działały tak, że jeśli dodasz pole do DTO, automatycznie zmieni on SQL używany do odczytu tych danych. Zatem masz dobry powód, by rozdzielić te dwa problemy.

Podsumowując, oto, co zwykle robię: jeśli istnieje przybliżona równowaga między liczbą razy, gdy mówię „nie, istnieje więcej niż jeden powód, aby zmienić ten obiekt”, a liczbą razy, gdy mówię „nie, ta zmiana będzie wpływa na więcej niż jeden obiekt ”, to myślę, że mam równowagę między SRP a fragmentacją. Ale jeśli oba są nadal wysokie, zaczynam się zastanawiać, czy istnieje inny sposób na rozróżnienie problemów.

pdr
źródło
+1 dla „czy każda zmiana dotyczy tylko jednej klasy?”
dj18
Powiązanym zagadnieniem, o którym nie mówiłem, jest to, że jeśli zadania powiązane z jedną jednostką logiczną zostaną podzielone między różne klasy, może być konieczne, aby kod zawierał odniesienia do wielu różnych obiektów, które wszystkie są powiązane z tym samym bytem. Rozważmy na przykład piec z funkcjami „SetHeaterOutput” i „MeasureTemperature”. Gdyby piec był reprezentowany przez niezależne obiekty HeaterControl i TemperatureSensor, wówczas nic nie przeszkodziłoby obiektowi TemperatureFeedbackSystem w utrzymywaniu odniesienia do grzejnika jednego pieca i innego czujnika temperatury pieca.
supercat
1
Jeśli zamiast tego funkcje te zostaną połączone w interfejs IKiln, który został zaimplementowany przez obiekt Kiln, wówczas TemperatureFeedbackSystem będzie musiał przechowywać tylko jedno odwołanie IKiln. Gdyby konieczne było użycie pieca z niezależnym czujnikiem temperatury na rynku wtórnym, można użyć obiektu CompositeKiln, którego konstruktor zaakceptował IHeaterControl i ITemperatureSensor i użył ich do implementacji IKiln, ale taki celowy luźny skład byłby łatwo rozpoznawalny w kodzie.
supercat
23

To, że system jest złożony, nie oznacza, że ​​musisz go skomplikować . Jeśli masz klasę, która ma zbyt wiele zależności (lub współpracowników), takich jak to:

public class MyAwesomeClass {
    public class MyAwesomeClass(IDependency1 _d1, IDependency2 _d2, ... , IDependency20 _d20) {
      // Assign it all
    }
}

... to stało się zbyt skomplikowane i tak naprawdę nie śledzisz SRP , prawda? Założę się, że jeśli zapisałeś, co MyAwesomeClassrobi na karcie CRC , nie zmieściłoby się na karcie indeksu lub będziesz musiał pisać naprawdę małymi, nieczytelnymi literami.

To, co tu masz, to fakt, że twoi faceci przestrzegali tylko zasady segregacji interfejsów i mogli doprowadzić do skrajności, ale to zupełnie inna historia. Można argumentować, że zależności są obiektami domeny (co się zdarza), jednak posiadanie klasy, która obsługuje 20 obiektów domeny w tym samym czasie, rozciąga je nieco za daleko.

TDD zapewni ci dobry wskaźnik tego, ile klasa robi. Mówiąc wprost; jeśli metoda testowa ma kod instalacyjny, którego pisanie trwa wiecznie (nawet jeśli refaktoryzujesz testy), to MyAwesomeClassprawdopodobnie masz zbyt wiele do zrobienia.

Jak więc rozwiązać tę zagadkę? Przenosisz obowiązki na inne klasy. Istnieje kilka kroków, które możesz podjąć w przypadku klasy, która ma ten problem:

  1. Zidentyfikuj wszystkie działania (lub obowiązki), które klasa wykonuje przy swoich zależnościach.
  2. Pogrupuj działania według ściśle powiązanych zależności.
  3. Redelegate! Oznacza to, że każdą ze zidentyfikowanych akcji przekieruj na nowe lub (co ważniejsze) inne klasy.

Abstrakcyjny przykład obowiązków związanych z refaktoryzacją

Niech Cbędzie, że klasa ma kilka zależności D1, D2, D3, D4że trzeba byłaby używać mniej. Kiedy ustalimy, jakie metody Cwywołują zależności, możemy utworzyć prostą listę:

  • D1- performA(D2),performB()
  • D2 - performD(D1)
  • D3 - performE()
  • D4 - performF(D3)

Patrząc na listę, widzimy to D1i D2są ze sobą spokrewnieni, ponieważ klasa potrzebuje ich jakoś razem. Możemy również zobaczyć te D4potrzeby D3. Mamy więc dwie grupy:

  • Group 1- D1<->D2
  • Group 2- D4->D3

Grupy są wskaźnikiem, że klasa ma teraz dwie obowiązki.

  1. Group 1- Jeden do obsługi wywoływania dwóch obiektów, które potrzebują siebie nawzajem. Być może możesz pozwolić swojej klasie Cwyeliminować potrzebę obsługi obu zależności i pozostawić jedną z nich obsługującą te połączenia. W tej grupie oczywiste jest, że D1może mieć odniesienie D2.
  2. Group 2- Druga odpowiedzialność potrzebuje jednego obiektu do wywołania drugiego. Nie możesz sobie D4poradzić D3zamiast swojej klasy? Wtedy prawdopodobnie możemy wyeliminować D3z klasy C, pozwalając D4zamiast tego wykonywać połączenia.

Nie bierz mojej odpowiedzi tak mocno osadzonej w kamieniu, ponieważ przykład jest bardzo abstrakcyjny i zawiera wiele założeń. Jestem prawie pewien, że istnieje więcej sposobów, aby to zmienić, ale przynajmniej te kroki mogą pomóc ci uzyskać jakiś proces przenoszenia obowiązków zamiast podziału klas.


Edytować:

Wśród komentarzy @Emmad Karem mówi:

„Jeśli twoja klasa ma 20 parametrów w konstruktorze, nie brzmi to tak, jakby Twój zespół wiedział, czym jest SRP. Jeśli masz klasę, która robi tylko jedną rzecz, to jak ma 20 zależności?” - myślę, że jeśli mieć klasę Customer, nie jest niczym dziwnym mieć 20 parametrów w konstruktorze.

Prawdą jest, że obiekty DAO mają zwykle wiele parametrów, które należy ustawić w konstruktorze, a parametry są zwykle prostymi typami, takimi jak łańcuch znaków. Jednak w przykładzie Customerklasy nadal można grupować jej właściwości w innych klasach, aby uprościć sprawę. Na przykład, mając Addressklasę z ulicami i Zipcodeklasę, która zawiera kod pocztowy i obsługuje logikę biznesową, taką jak sprawdzanie poprawności danych:

public class Address {
    private String street1;
    //...

    private Zipcode zipcode;

    // easy to extend
    public bool isValid() {
        return zipcode.isValid();
    }
}

public class Zipcode {
    private string zipcode;
    public bool isValid() {
        // return regex match that zipcode contains numbers
    }
}

Zagadnienie to zostało omówione w poście na blogu „Nigdy, nigdy, nigdy nie używaj Stringa w Javie (lub przynajmniej często)” . Alternatywą dla użycia konstruktorów lub metod statycznych w celu ułatwienia tworzenia obiektów podrzędnych jest użycie wzorca konstruktora płynów .

Łup
źródło
+1: Świetna odpowiedź! Grupowanie jest IMO bardzo potężnym mechanizmem, ponieważ można zastosować grupowanie rekurencyjne. Mówiąc bardzo szorstko, z n warstwami abstrakcji możesz uporządkować 2 ^ n elementów.
Giorgio
+1: Pierwsze kilka akapitów podsumowuje dokładnie to, przed czym stoi mój zespół. „Obiekty biznesowe”, które w rzeczywistości są obiektami usługowymi, oraz kod konfiguracji testu jednostkowego, który jest odrętwiający, aby napisać. Wiedziałem, że mamy problem z tym, że nasze wywołania warstwy usług zawierają jeden wiersz kodu; wywołanie metody warstwy biznesowej.
Mężczyzna,
3

Zgadzam się ze wszystkimi odpowiedziami na temat SRP i tego, jak można go posunąć za daleko. W swoim poście wspominasz, że z powodu „nadmiernego refaktoryzacji”, aby zastosować się do SRP, znalazłeś łamanie lub modyfikowanie enkapsulacji. Jedyną rzeczą, która mi się sprawdziła, jest trzymanie się podstaw i robienie dokładnie tego, co jest wymagane do osiągnięcia celu.

Podczas pracy z systemami Legacy „entuzjazm” do naprawiania wszystkiego, aby było lepiej, jest zwykle dość wysoki w zespołach kierowniczych, szczególnie tych, którzy są nowi w tej roli. SOLID, po prostu nie ma SRP - to tylko S. Upewnij się, że jeśli podążasz za SOLID, nie zapomnisz również OLID.

Obecnie pracuję nad systemem Legacy i na początku zaczęliśmy podążać podobną ścieżką. Dla nas zadziałała wspólna decyzja zespołu, aby jak najlepiej wykorzystać oba światy - SOLID i KISS (Keep It Simple Stupid). Wspólnie dyskutowaliśmy o najważniejszych zmianach w strukturze kodu i stosowaliśmy zdrowy rozsądek w stosowaniu różnych zasad programistycznych. Są świetne jako wytyczne, a nie „Prawa rozwoju S / W”. W zespole nie chodzi tylko o kierownika zespołu - chodzi o wszystkich programistów w zespole. Rzeczą, która zawsze działała dla mnie, jest doprowadzenie wszystkich do pokoju i przedstawienie wspólnego zestawu wytycznych, których cały zespół zgodzi się przestrzegać.

Jeśli korzystasz z VCS i nie dodajesz zbyt wielu nowych funkcji do swojej aplikacji, jeśli chcesz naprawić swoją obecną sytuację, zawsze możesz wrócić do wersji kodu, którą cały zespół uważa za zrozumiały, czytelny i łatwy do utrzymania. Tak! Proszę cię, odrzuć pracę i zacznij od zera. Jest to lepsze niż próba „naprawienia” czegoś, co zostało zepsute i przeniesienie go z powrotem do czegoś, co już istniało.

Sharath Satish
źródło
3

Odpowiedzią jest przede wszystkim łatwość konserwacji i przejrzystość kodu. Dla mnie oznacza to pisanie mniej kodu , a nie więcej. Mniej abstrakcji, mniej interfejsów, mniej opcji, mniej parametrów.

Ilekroć oceniam restrukturyzację kodu lub dodając nową funkcję, myślę o tym, ile będzie wymagało płyta podstawowa w porównaniu do rzeczywistej logiki. Jeśli odpowiedź jest większa niż 50%, prawdopodobnie oznacza to, że zastanawiam się nad tym.

Oprócz SRP istnieje wiele innych stylów programowania. W twoim przypadku zdecydowanie brakuje brzmień YAGNI.

cmcginty
źródło
3

Wiele odpowiedzi tutaj jest naprawdę dobrych, ale koncentruje się na technicznej stronie tego problemu. Dodam po prostu, że brzmi to tak, jakby deweloper próbował podążać za dźwiękiem SRP, jakby faktycznie naruszał SRP.

Możesz zobaczyć blog Boba tutaj na temat tej sytuacji, ale twierdzi on, że jeśli odpowiedzialność jest rozmazana w wielu klasach, odpowiedzialność SRP zostaje naruszona, ponieważ klasy te zmieniają się równolegle. Podejrzewam, że twój twórca naprawdę spodobałby się projektowi na szczycie bloga Boba i może być trochę rozczarowany widząc, że został on rozerwany. W szczególności dlatego, że narusza „wspólną zasadę zamknięcia” - rzeczy, które zmieniają się razem, pozostają razem.

Pamiętaj, że SRP odnosi się do „przyczyny zmiany”, a nie „rób jedną rzecz”, i że nie musisz się martwić tym powodem zmiany, dopóki zmiana faktycznie nie nastąpi. Drugi facet płaci za abstrakcję.

Teraz jest drugi problem - „zjadliwy zwolennik rozwoju SOLID”. Z pewnością nie brzmi to tak, jakbyś miał świetne relacje z tym deweloperem, więc wszelkie próby przekonania go o problemach w bazie kodu są zakłócone. Musisz naprawić relację, aby móc prawdziwie omówić problemy. Polecam piwo.

Nie poważnie - jeśli nie pijesz, udaj się do kawiarni. Wyjdź z biura i zrelaksuj się, gdzie możesz porozmawiać o tym nieformalnie. Zamiast próbować wygrać kłótnię na spotkaniu, czego nie chcesz, dyskutuj gdzieś zabawnie. Spróbuj rozpoznać, że ten deweloper, który doprowadza cię do szału, jest faktycznie działającym człowiekiem, który próbuje wydostać się z oprogramowania i nie chce wysyłać bzdur. Ponieważ prawdopodobnie podzielasz tę wspólną płaszczyznę, możesz zacząć dyskutować, jak ulepszyć projekt, zachowując jednocześnie zgodność z SRP.

Jeśli oboje możecie uznać, że SRP jest dobrą rzeczą, że po prostu interpretujecie aspekty inaczej, prawdopodobnie możecie zacząć produktywne rozmowy.

Eric Smith
źródło
-1

Zgadzam się z twoją decyzją lidera zespołu [aktualizacja = 2012.05.31], że SRP jest ogólnie dobrym pomysłem. Ale całkowicie zgadzam się z komentarzem @ Spoike -s, że konstruktor z 20 argumentami interfejsu jest zbyt wiele. [/ Update]:

Wprowadzenie SRP z IoC przenosi złożoność z jednej „klasy wielu odpowiedzialnych” do wielu klas srp i znacznie bardziej skomplikowaną inicjalizację z korzyścią dla

  • łatwiejsza testowalność jednostkowa / tdd (testowanie pojedynczej klasy srp jednocześnie)
  • ale kosztem
    • znacznie trudniejsza inicjalizacja i integracja kodu oraz
    • więcej trudnych debugowania
    • fragmentacja (= dystrybucja kodu w kilku plikach / katalogach)

Obawiam się, że nie można zmniejszyć defragmentacji kodu bez poświęcenia srp.

Ale możesz „złagodzić ból” inicjalizacji kodu poprzez zaimplementowanie syntaktycznej klasy cukru, która ukrywa złożoność inicjalizacji w konstruktorze.

   class MySrpClass {
      MySrpClass(Interface1 parm1, Interface2 param2, .... Interface20 param2) {
      }
   } 

   class MySyntaxSugarClass : MySrpClass {
      MySyntaxSugarClass() {
         super(new MyInterface1Implementation(), new MyImpl2(), ....)
      }
   }
k3b
źródło
2
Uważam, że 20 interfejsów jest wskaźnikiem, że klasa ma zbyt wiele do zrobienia. Tzn. Jest 20 powodów, aby to zmienić, co jest w zasadzie pogwałceniem SRP. To, że system jest złożony, nie oznacza, że ​​musi być skomplikowane.
Spoike