Czy zasada / jedna zasada powinna mieć zastosowanie do nowego kodu?

20

Zasada jest zdefiniowana jako moduły mające jeden powód do zmiany . Moje pytanie brzmi: z pewnością te przyczyny zmiany nie są znane, dopóki kod nie zacznie się zmieniać? Prawie każdy fragment kodu ma wiele powodów, dla których mógłby się zmienić, ale z pewnością próba przewidzenia ich wszystkich i zaprojektowania kodu z myślą o tym skończyłaby się bardzo słabym kodem. Czy nie jest lepszym pomysłem, aby naprawdę zacząć stosować SRP tylko wtedy, gdy przychodzą żądania zmiany kodu? Mówiąc dokładniej, gdy fragment kodu zmienił się więcej niż jeden raz z więcej niż jednego powodu, co dowodzi, że ma więcej niż jeden powód do zmiany. Próba odgadnięcia powodów zmian wydaje się bardzo anty-zwinna.

Przykładem może być fragment kodu, który drukuje dokument. Pojawia się prośba o zmianę w celu wydrukowania do formatu PDF, a następnie pojawia się drugie żądanie zmiany w celu zastosowania innego formatowania do dokumentu. W tym momencie masz dowód na więcej niż jeden powód zmiany (i naruszenia SRP) i powinieneś dokonać odpowiedniego refaktoryzacji.

SeeNoWeevil
źródło
6
@Frank - tak naprawdę jest tak powszechnie definiowane - patrz np. En.wikipedia.org/wiki/Single_responsibility_principle
Joris Timmermans
1
Sposób, w jaki piszesz to nie jest sposób, w jaki rozumiem definicję SRP.
Pieter B
2
Każdy wiersz kodu ma (co najmniej) dwa powody zmiany: Przyczynia się do błędu lub zakłóca nowe wymaganie.
Bart van Ingen Schenau
1
@BartvanIngenSchenau: LOL ;-) Jeśli zobaczysz to w ten sposób, SRP nie będzie można nigdzie zastosować.
Doc Brown
1
@DocBrown: Możesz, jeśli nie sparujesz SRP ze zmianą kodu źródłowego. Na przykład, jeśli interpretujesz SRP jako zdolność do pełnego opisania tego, co klasa / funkcja robi w jednym zdaniu bez użycia słowa i (i nie ma sformułowania łasicy, aby obejść to ograniczenie).
Bart van Ingen Schenau

Odpowiedzi:

27

Oczywiście zasada YAGNI mówi ci, abyś stosował SRP nie zanim naprawdę tego potrzebujesz. Ale pytanie, które powinieneś sobie zadać, brzmi: czy muszę najpierw zastosować SRP i tylko wtedy, gdy muszę zmienić kod?

Z mojego doświadczenia wynika, że ​​zastosowanie SRP daje korzyść znacznie wcześniej: kiedy musisz dowiedzieć się, gdzie i jak zastosować określoną zmianę w kodzie. Aby wykonać to zadanie, musisz przeczytać i zrozumieć swoje istniejące funkcje i klasy. Staje się to znacznie łatwiejsze, gdy wszystkie twoje funkcje i klasy ponoszą szczególną odpowiedzialność. Więc IMHO powinieneś stosować SRP, ilekroć ułatwia to odczytanie kodu, ilekroć sprawia, że ​​twoje funkcje są mniejsze i bardziej samoopisujące się. Tak więc odpowiedź brzmi tak , sensowne jest zastosowanie SRP nawet dla nowego kodu.

Na przykład, gdy kod drukujący odczytuje dokument, formatuje dokument i drukuje wynik na określonym urządzeniu, są to 3 wyraźne obowiązki do rozdzielenia. Wykonaj z nich co najmniej 3 funkcje, nadaj im odpowiednie nazwy. Na przykład:

 void RunPrintWorkflow()
 {
     var document = ReadDocument();
     var formattedDocument = FormatDocument(document);
     PrintDocumentToScreen(formattedDocument);
 }

Teraz, gdy pojawi się nowy wymóg zmiany formatowania dokumentu lub inny, aby wydrukować do formatu PDF, wiesz dokładnie, w której z tych funkcji lub lokalizacji w kodzie musisz zastosować zmiany, a co ważniejsze, gdzie nie.

Tak więc, gdy przyjdziesz do funkcji nie rozumiesz, ponieważ funkcja ma „za dużo”, a ty nie jesteś pewien, czy i gdzie zastosować zmiany, następnie rozważenia byłaby funkcję w oddzielnych, mniejszych funkcji. Nie czekaj, aż będziesz musiał coś zmienić. Kod jest 10 razy częściej odczytywany niż zmieniany, a mniejsze funkcje są znacznie łatwiejsze do odczytania. Z mojego doświadczenia wynika, że ​​gdy funkcja ma pewną złożoność, zawsze możesz podzielić funkcję na różne obowiązki, niezależnie od tego, jakie zmiany nadejdą w przyszłości. Bob Martin zazwyczaj idzie o krok dalej, zobacz link podany w moich komentarzach poniżej.

EDYCJA: do komentarza: Główną odpowiedzialnością funkcji zewnętrznej w powyższym przykładzie nie jest drukowanie na określonym urządzeniu ani formatowanie dokumentu - jest to integracja procesu drukowania . Zatem na poziomie abstrakcji funkcji zewnętrznej nowy wymóg, taki jak „dokumenty nie powinny być już formatowane” lub „dokumenty powinny być wysyłane zamiast drukowane”, jest po prostu „tym samym powodem” - mianowicie „przepływ pracy drukowania się zmienił”. Jeśli mówimy o takich rzeczach, ważne jest, aby trzymać się właściwego poziomu abstrakcji .

Doktor Brown
źródło
Generalnie zawsze rozwijam z TDD, więc w moim przykładzie fizycznie nie byłbym w stanie zachować całej logiki w jednym module, ponieważ testowanie byłoby niemożliwe. To tylko produkt uboczny TDD, a nie dlatego, że celowo stosuję SRP. Mój przykład miał dość jasne, oddzielne obowiązki, więc może nie jest to dobry przykład. Myślę, że pytam, czy możesz napisać nowy fragment kodu i jednoznacznie powiedzieć: tak, to nie narusza SRP? Czy „powody do zmiany” nie są zasadniczo zdefiniowane przez firmę?
SeeNoWeevil,
3
@thecapsaicinkid: tak, możesz (przynajmniej przez natychmiastowe refaktoryzacja). Ale dostaniesz bardzo, bardzo małe funkcje - i nie każdemu programiście to się podoba. Zobacz ten przykład: sites.google.com/site/unclebobconsultingllc/…
Doc Brown
Jeśli stosujesz SRP, przewidując powody zmiany, w twoim przykładzie nadal mogę argumentować, że ma więcej niż jedną zmianę przyczyny. Firma może zdecydować, że nie chce już formatować dokumentu, a następnie zdecydować, że chce, aby był on wysyłany pocztą elektroniczną zamiast drukowany. EDYCJA: Po prostu przeczytaj link i chociaż nie podoba mi się wynik końcowy, „Wyodrębniaj, dopóki nie będziesz mógł już wyodrębnić” ma znacznie większy sens i jest mniej dwuznaczny niż „tylko jeden powód do zmiany”. Jednak niezbyt pragmatyczny.
SeeNoWeevil
1
@thecapsaicinkid: zobacz moją edycję. Główną odpowiedzialnością funkcji zewnętrznej nie jest drukowanie na określonym urządzeniu ani formatowanie dokumentu - to integracja przepływu pracy drukowania. A kiedy ten przepływ pracy się zmienia, jest to jedyny powód, dla którego funkcja się zmieni
Doc Brown
Twój komentarz dotyczący trzymania się właściwego poziomu abstrakcji wydaje się być tym, czego mi brakowało. Na przykład mam klasę, którą opisałbym jako „Tworzy struktury danych z tablicy JSON”. Brzmi dla mnie jak jedna odpowiedzialność. Pętle przechodzą przez obiekty w tablicy JSON i mapują je na POJO. Jeśli trzymam się tego samego poziomu abstrakcji, co mój opis, trudno argumentować, że ma więcej niż jeden powód do zmiany, tj. „Jak JSON mapuje na obiekt”. Będąc mniej abstrakcyjnym, mógłbym argumentować, że ma więcej niż jeden powód, np. Jak
mapuję
7

Myślę, że nie rozumiesz SRP.

Jedynym powodem zmiany NIE jest zmiana kodu, ale to, co robi Twój kod.

Pieter B.
źródło
3

Myślę, że definicja SRP jako „mającego jeden powód do zmiany” jest myląca z tego właśnie powodu. Przyjmij to dokładnie na pierwszy rzut oka: zasada pojedynczej odpowiedzialności mówi, że klasa lub funkcja powinna ponosić dokładnie jedną odpowiedzialność. Posiadanie tylko jednego powodu do zmiany jest efektem ubocznym zrobienia tylko jednej rzeczy na początek. Nie ma powodu, dla którego nie możesz przynajmniej podjąć wysiłku na rzecz odpowiedzialności w kodzie bez wiedzy o tym, jak może się to zmienić w przyszłości.

Jedną z najlepszych wskazówek tego rodzaju jest wybór nazw klas lub funkcji. Jeśli nie jest od razu oczywiste, jak powinna być nazwana klasa, lub nazwa jest szczególnie długa / złożona, lub nazwa używa ogólnych terminów, takich jak „manager” lub „narzędzie”, to prawdopodobnie narusza SRP. Podobnie podczas dokumentowania interfejsu API powinno szybko stać się jasne, jeśli naruszasz SRP w oparciu o opisywaną funkcjonalność.

Istnieją oczywiście niuanse w SRP, których nie znasz do końca projektu - coś, co wydawało się jedną odpowiedzialnością, okazało się dwie lub trzy. Są to przypadki, w których będziesz musiał refaktoryzować, aby wdrożyć SRP. Ale to nie znaczy, że SRP powinno być ignorowane, dopóki nie pojawi się prośba o zmianę; który pokonuje cel SRP!

Aby mówić bezpośrednio do swojego przykładu, rozważ udokumentowanie metody drukowania. Jeśli chcesz powiedzieć „metoda ta formatuje dane do drukowania i wysyła go do drukarki”, że i to, co dostaje się: to nie jest pojedynczy odpowiedzialność, to dwa zadania: Formatowanie i wysyłanie do drukarki. Jeśli rozpoznasz to i podzielisz je na dwie funkcje / klasy, to kiedy pojawią się twoje żądania zmiany, będziesz miał tylko jeden powód do zmiany każdej sekcji.

Adrian
źródło
3

Przykładem może być fragment kodu, który drukuje dokument. Pojawia się prośba o zmianę w celu wydrukowania do formatu PDF, a następnie pojawia się drugie żądanie zmiany w celu zastosowania innego formatowania do dokumentu. W tym momencie masz dowód na więcej niż jeden powód zmiany (i naruszenia SRP) i powinieneś dokonać odpowiedniego refaktoryzacji.

Tak wiele razy postrzeliłem się w stopę, spędzając zbyt dużo czasu na dostosowywaniu kodu do tych zmian. Zamiast drukować cholernie głupi PDF.

Refaktoryzacja w celu zmniejszenia kodu

Wzorzec jednorazowego użytku może tworzyć wzdęcia kodu. Gdzie pakiety są zanieczyszczone małymi określonymi klasami, które tworzą stos śmieci, który nie ma sensu indywidualnie. Musisz otworzyć dziesiątki plików źródłowych, aby zrozumieć, w jaki sposób dociera do części drukującej. Ponadto mogą istnieć setki, jeśli nie tysiące wierszy kodu, które służą tylko do wykonania 10 wierszy kodu, które faktycznie drukują.

Utwórz Bullseye

Wzorzec jednorazowego użytku miał na celu ograniczenie kodu źródłowego i poprawę ponownego wykorzystania kodu. Miało to na celu stworzenie specjalizacji i konkretnych wdrożeń. Coś bullseyew kodzie źródłowym dla ciebie go to specific tasks. Kiedy pojawił się problem z drukowaniem, wiedziałeś dokładnie, gdzie go naprawić.

Pojedyncze użycie nie oznacza niejednoznacznego szczelinowania

Tak, masz kod, który już drukuje dokument. Tak, musisz teraz zmienić kod, aby drukować również pliki PDF. Tak, musisz teraz zmienić formatowanie dokumentu.

Czy jesteś pewien, że usageznacznie się zmieniło?

Jeśli refaktoryzacja powoduje nadmierne uogólnienie sekcji kodu źródłowego. Do tego stopnia, że ​​pierwotna intencja printing stuffnie jest już jednoznaczna, to stworzyliście niejednoznaczne pękanie w kodzie źródłowym.

Czy nowy facet będzie w stanie szybko to rozgryźć?

Zawsze utrzymuj swój kod źródłowy w najłatwiejszej do zrozumienia organizacji.

Nie bądź zegarmistrzem

Zbyt wiele razy widziałem, jak programiści zakładali okular i skupiali się na drobnych szczegółach do tego stopnia, że ​​nikt inny nie byłby w stanie złożyć tych elementów ponownie, gdyby się rozpadło.

wprowadź opis zdjęcia tutaj

Reactgular
źródło
2

Przyczyną zmiany jest ostatecznie zmiana specyfikacji lub informacji o środowisku, w którym działa aplikacja. Zasada jednej odpowiedzialności nakazuje Ci napisać każdy komponent (klasę, funkcję, moduł, usługę ...), aby musiał uwzględniać możliwie najmniej specyfikacji i środowiska wykonawczego.

Ponieważ podczas pisania komponentu znasz specyfikację i środowisko, możesz zastosować tę zasadę.

Jeśli weźmiesz przykład kodu, który drukuje dokument. Zastanów się, czy możesz zdefiniować szablon układu bez uwzględnienia dokumentu w formacie PDF. Możesz, więc SRP mówi ci, że powinieneś.

Oczywiście YAGNI mówi ci, że nie powinieneś. Musisz znaleźć równowagę między zasadami projektowania.

Jan Hudec
źródło
2

Flup zmierza we właściwym kierunku. „Zasada jednolitej odpowiedzialności” pierwotnie obowiązywała w procedurach. Na przykład Dennis Ritchie powiedziałby, że funkcja powinna zrobić jedną rzecz i zrobić to dobrze. Następnie w C ++ Bjarne Stroustrup powiedziałby, że klasa powinna zrobić jedną rzecz i zrobić to dobrze.

Zauważ, że z wyjątkiem praktycznych reguł, ci dwaj formalnie mają ze sobą niewiele lub nic wspólnego. Zaspokajają tylko to, co jest wygodne do wyrażenia w języku programowania. Cóż, to jest coś. Ale to zupełnie inna historia niż to, do czego zmierza flup.

Nowoczesne implementacje (tj. Zwinne i DDD) skupiają się bardziej na tym, co jest ważne dla biznesu niż na tym, co może wyrazić język programowania. Zaskakujące jest to, że języki programowania jeszcze nie nadrobiły zaległości. Stare języki podobne do FORTRAN przechwytują obowiązki, które pasują do głównych modeli koncepcyjnych tamtych czasów: procesów, które stosowano do każdej karty, gdy przechodziła ona przez czytnik kart, lub (jak w C) przetwarzania towarzyszącego każdemu przerwaniu. Potem pojawiły się języki ADT, które dojrzewały do ​​tego stopnia, że ​​uchwyciły to, co ludzie DDD wymyślili na nowo jako ważne (chociaż Jim Neighbors większość tego zorientowali się, opublikowali i używali do 1968 roku): co dziś nazywamy klasami . (To NIE są moduły.)

Ten krok był mniej ewolucyjny niż wahadło wahadłowe. Gdy wahadło przeszło do danych, straciliśmy modelowanie przypadków użycia właściwe dla FORTRAN. To dobrze, gdy głównym celem są dane lub kształty na ekranie. To świetny model dla programów takich jak PowerPoint, a przynajmniej dla jego prostych operacji.

Zgubiono obowiązki systemowe . Nie sprzedajemy elementów DDD. Nie radzimy sobie z metodami klasowymi. Sprzedajemy obowiązki systemowe. Na pewnym poziomie musisz zaprojektować swój system zgodnie z zasadą jednej odpowiedzialności.

Więc jeśli spojrzysz na ludzi takich jak Rebecca Wirfs-Brock lub ja, którzy mówili o metodach klasowych, teraz rozmawiamy o przypadkach użycia. Właśnie to sprzedajemy. To są operacje systemowe. Przypadek użycia powinien ponosić jedną odpowiedzialność. Przypadek użycia rzadko jest jednostką architektoniczną. Ale wszyscy próbowali udawać, że tak jest. Na przykład obserwuj ludzi SOA.

Dlatego jestem podekscytowany architekturą DCI Trygve'a Reenskauga - tak opisałem powyższą książkę Lean Architecture. W końcu nadaje prawdziwą rangę temu, co kiedyś było arbitralnym i mistycznym posłuszeństwem wobec „pojedynczej odpowiedzialności” - jak można znaleźć w większości powyższych argumentów. Ta postawa odnosi się do ludzkich modeli mentalnych: najpierw użytkownicy końcowi ORAZ programiści na drugim miejscu. Dotyczy problemów biznesowych. I prawie przypadkiem obejmuje zmiany, gdy flup rzuca nam wyzwanie.

Zasada pojedynczej odpowiedzialności, jaką znamy, jest albo dinozaurem pozostałym po jego pochodzeniu, albo koniem hobbystycznym, którego używamy jako substytutu zrozumienia. Musisz zostawić kilka z tych hobby koni, aby zrobić świetne oprogramowanie. A to wymaga myślenia od razu po wyjęciu z pudełka. Utrzymanie prostoty i łatwości zrozumienia działa tylko wtedy, gdy problem jest prosty i łatwy do zrozumienia. Nie interesują mnie te rozwiązania: nie są typowe i nie na tym polega wyzwanie.

Sprostać
źródło
2
Czytając to, co napisałeś, gdzieś po prostu całkowicie straciłem z oczu to, o czym mówisz. Dobre odpowiedzi nie traktują pytania jako punktu wyjścia do wędrówki po lesie, ale raczej jako określonego tematu, do którego należy połączyć całe pisanie.
Donal Fellows
1
Ach, jesteś jednym z nich, jak jeden z moich starych menedżerów. „Nie chcemy tego rozumieć: chcemy to poprawić!” Kluczową kwestią tematyczną jest tutaj jedna z zasad: to „P” w „SRP”. Być może odpowiedziałbym bezpośrednio na pytanie, gdyby było to właściwe pytanie: nie było. Możesz poradzić sobie z tym, kto kiedykolwiek zadał pytanie.
Cope,
Gdzieś tu jest zakopana dobra odpowiedź. Myślę ...
RubberDuck,
0

Tak, zasada nowego zakresu odpowiedzialności powinna być stosowana do nowego kodu.

Ale! Co to jest odpowiedzialność?

Czy „drukuje raport jest obowiązkiem”? Uważam, że odpowiedź brzmi „może”.

Spróbujmy użyć definicji SRP jako „mającego tylko jeden powód do zmiany”.

Załóżmy, że masz funkcję drukowania raportów. Jeśli masz dwie zmiany:

  1. zmień tę funkcję, ponieważ raport musi mieć czarne tło
  2. zmień tę funkcję, ponieważ musisz wydrukować do formatu pdf

Następnie pierwszą zmianą jest „zmiana stylu raportu”, drugą „zmiana formatu wyjściowego raportu”, a teraz powinieneś umieścić je w dwóch różnych funkcjach, ponieważ są to różne rzeczy.

Ale jeśli twoją drugą zmianą byłoby:

2b. zmień tę funkcję, ponieważ Twój raport wymaga innej czcionki

Powiedziałbym, że obie zmiany „zmieniają styl raportu” i mogą pozostać w jednej funkcji.

Więc gdzie nas to opuszcza? Jak zwykle powinieneś starać się zachować prostotę i łatwość zrozumienia. Jeśli zmiana koloru tła oznacza 20 linii kodu, a zmiana czcionki oznacza 20 linii kodu, spraw, by znów działały dwie funkcje. Jeśli jest to jedna linia, trzymaj ją w jednym.

Sarien
źródło
0

Projektując nowy system, dobrze jest wziąć pod uwagę rodzaj zmian, które możesz wprowadzić w trakcie jego życia, oraz to, jak drogie będą one związane z architekturą, którą wprowadzasz. Podział systemu na moduły to kosztowna decyzja, aby się pomylić.

Dobrym źródłem informacji jest model mentalny w głowie ekspertów z dziedziny biznesu. Weź przykład dokumentu, formatowania i pdf. Eksperci domeny prawdopodobnie powiedzą, że formatują swoje litery przy użyciu szablonów dokumentów. Albo stacjonarnie, albo w słowie, czy cokolwiek innego. Możesz pobrać te informacje przed rozpoczęciem kodowania i wykorzystać je w swoim projekcie.

Świetna lektura na ten temat: Lean Architecture autorstwa Coplien

flup
źródło
0

„Drukuj” jest bardzo podobne do „widoku” w MVC. Każdy, kto rozumie podstawy przedmiotów, zrozumie to.

Jest to odpowiedzialność systemu . Jest zaimplementowany jako mechanizm - MVC - który obejmuje drukarkę (Widok), drukowaną rzecz (Moduł) oraz żądanie i opcje drukarki (z kontrolera).

Próba ustalenia tego jako odpowiedzialności za klasę lub moduł jest tak samo oczywista i odzwierciedla myślenie 30-letnie. Od tego czasu wiele się nauczyliśmy i jest to dobrze udokumentowane w literaturze i kodzie dojrzałych programistów.

Sprostać
źródło
0

Czy nie jest lepszym pomysłem, aby naprawdę zacząć stosować SRP tylko wtedy, gdy przychodzą żądania zmiany kodu?

Idealnie byłoby, gdybyś już miał dobry pomysł na obowiązki poszczególnych części kodu. Podziel się na obowiązki zgodnie z twoimi pierwszymi instynktami, prawdopodobnie biorąc pod uwagę to, czego chcą biblioteki, których używasz (delegowanie zadania, odpowiedzialności, do biblioteki jest zwykle świetną rzeczą do zrobienia, pod warunkiem, że biblioteka może faktycznie wykonać zadanie ). Następnie udoskonal swoje rozumienie obowiązków zgodnie ze zmieniającymi się wymaganiami. Im lepiej rozumiesz system na początku, tym mniej potrzebujesz fundamentalnej zmiany przypisań odpowiedzialności (choć czasami odkrywasz, że odpowiedzialność najlepiej podzielić na podzadania).

Nie dlatego, że powinieneś długo się martwić. Kluczową cechą kodu jest to, że można go później zmienić, nie musisz go poprawiać za pierwszym razem. Po prostu postaraj się z czasem poprawić swoją wiedzę na temat obowiązków związanych z kształtowaniem, abyś mógł popełnić mniej błędów w przyszłości.

Przykładem może być fragment kodu, który drukuje dokument. Pojawia się prośba o zmianę w celu wydrukowania do formatu PDF, a następnie pojawia się drugie żądanie zmiany w celu zastosowania innego formatowania do dokumentu. W tym momencie masz dowód na więcej niż jeden powód zmiany (i naruszenia SRP) i powinieneś dokonać odpowiedniego refaktoryzacji.

Jest to ściśle wskazówka, że ​​ogólna odpowiedzialność - „wydrukowanie” kodu - ma dodatkowe obowiązki i powinna zostać podzielona na części. To nie jest naruszenie SRP per se , ale raczej wskazanie, że partycjonowanie (być może do „formatowania” i „rendering” podzadań) jest prawdopodobnie wymagane. Czy potrafisz jasno opisać te obowiązki, aby zrozumieć, co się dzieje w ramach pod zadań, nie patrząc na ich realizację? Jeśli możesz, prawdopodobnie będą to rozsądne podziały.

Może być również jaśniejsze, jeśli spojrzymy na prosty prawdziwy przykład. Rozważmy sort()metodę użyteczności w java.util.Arrays. Co to robi? Sortuje tablicę i to wszystko. Nie drukuje elementów, nie znajduje najbardziej moralnie dopasowanego członka, nie gwizda Dixie . Po prostu sortuje tablicę. Nie musisz też wiedzieć, jak to zrobić. Sortowanie jest jedyną odpowiedzialnością tej metody. (W rzeczywistości istnieje wiele metod sortowania w Javie z nieco brzydkich powodów technicznych związanych z typami pierwotnymi; nie musisz jednak na to zwracać uwagi, ponieważ wszystkie mają równoważne obowiązki).

Uczyńcie swoje metody, klasy, moduły, aby miały one tak wyraźnie wyznaczoną rolę w życiu. Zmniejsza to kwotę, którą musisz zrozumieć od razu, a to z kolei pozwala ci poradzić sobie z projektowaniem i utrzymaniem dużego systemu.

Donal Fellows
źródło