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.
źródło
Odpowiedzi:
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:
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 .
źródło
Myślę, że nie rozumiesz SRP.
Jedynym powodem zmiany NIE jest zmiana kodu, ale to, co robi Twój kod.
źródło
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.
źródło
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ś
bullseye
w kodzie źródłowym dla ciebiego 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
usage
znacznie się zmieniło?Jeśli refaktoryzacja powoduje nadmierne uogólnienie sekcji kodu źródłowego. Do tego stopnia, że pierwotna intencja
printing stuff
nie jest już jednoznaczna, to stworzyliście niejednoznaczne pękanie w kodzie źródłowym.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.
źródło
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.
źródło
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.
źródło
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:
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.
źródło
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
źródło
„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.
źródło
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.
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 wjava.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.
źródło