Dlaczego warto stosować wzorzec strategii, jeśli można po prostu napisać kod w przypadkach „jeśli / to”?
Na przykład: Mam klasę TaxPayer, a jedna z jej metod oblicza podatki przy użyciu różnych algorytmów. Dlaczego więc nie może mieć przypadków if / then i dowiedzieć się, jakiego algorytmu użyć w tej metodzie, zamiast używać wzorca strategii? Dlaczego nie możesz po prostu zaimplementować osobnej metody dla każdego algorytmu w klasie TaxPayer?
Co to znaczy, że algorytm zmienia się w czasie wykonywania?
Odpowiedzi:
Po pierwsze, duże skupiska
if/else
bloków nie są łatwe do przetestowania . Każda nowa „gałąź” dodaje inną ścieżkę wykonania, a tym samym zwiększa złożoność cykliczną . Jeśli chcesz dokładnie przetestować swój kod, musisz pokryć wszystkie ścieżki wykonania, a każdy warunek wymagałby napisania co najmniej jednego testu (zakładając, że piszesz małe, ukierunkowane testy). Z drugiej strony klasy wdrażające strategie zazwyczaj ujawniają tylko 1 metodę publiczną, którą łatwo przetestować.Dzięki zagnieżdżeniu
if/else
skończysz wiele testów dla jednej części kodu, podczas gdy w Strategii będziesz mieć kilka testów dla każdej z wielu prostszych strategii. W tym drugim przypadku łatwiej jest uzyskać lepszy zasięg, ponieważ trudniej jest pominąć ścieżki wykonania.Jeśli chodzi o rozszerzalność , wyobraź sobie, że piszesz platformę, w której użytkownicy powinni mieć możliwość wprowadzenia własnych zachowań. Na przykład, chcesz stworzyć pewien rodzaj ram obliczania podatków i chcesz wspierać systemy podatkowe różnych krajów. Zamiast wdrożyć je wszystkie, po prostu chcesz dać użytkownikom frameworków szansę na wdrożenie sposobu obliczania niektórych określonych podatków.
Oto wzorzec strategii:
TaxCalculation
a twoja struktura akceptuje instancje tego typu do obliczania podatkówNie możesz zrobić tego samego
if/else
, ponieważ wymagałoby to zmiany kodu frameworka, w którym to przypadku nie byłby już frameworkiem. Ponieważ frameworki są często dystrybuowane w formie skompilowanej, może to być jedyna opcja.Mimo to, nawet jeśli piszesz zwykły kod, Strategia jest korzystna, ponieważ sprawia, że twoje zamiary stają się wyraźniejsze. Mówi „ta logika jest podłączalna i warunkowa”, tzn. Może istnieć wiele implementacji, które mogą się różnić w zależności od działań użytkownika, konfiguracji, a nawet platformy.
Stosując wzór strategia może poprawić czytelność , ponieważ, podczas gdy klasa który realizuje jakąś szczególną strategię zazwyczaj powinien mieć nazwę opisową, na przykład
USAIncomeTaxCalculator
,if/else
bloki są „bezimienny”, w najlepszym przypadku tylko komentuje i komentarze mogą kłamać. Ponadto, według mojego osobistego gustu, posiadanie więcej niż 3if/else
bloków z rzędu jest nieczytelne i robi się całkiem źle z zagnieżdżonymi blokami.Zasada Otwarta / Zamknięta jest również bardzo istotna, ponieważ, jak opisałem w powyższym przykładzie, Strategia pozwala rozszerzyć logikę w niektórych częściach twojego kodu („otwarta na rozszerzenie”) bez przepisywania tych części („zamknięta dla modyfikacji” ).
źródło
if/else
bloki również obniżają czytelność kodu. Jeśli chodzi o wzorzec strategii, warto wspomnieć o IMO.if
masz, tym więcej możliwych ścieżek istnieje w kodzie, tym więcej testów musisz napisać i więcej sposobów na niepowodzenie tej metody. Jeśli mogę zacytować zmarłego Yogi Berrę: „Jeśli dojdziesz do rozwidlenia drogi, weź to”. Dotyczy to doskonale testów jednostkowych. Co więcej, wieleif
stwierdzeń oznacza, że prawdopodobnie powtórzysz logikę dla tych warunków, dodatkowo zwiększając obciążenie testowe i zwiększając ryzyko pojawienia się błędów.if/else
bloków, aby do nich zadzwonić (lub wewnątrz nich, aby ustalić, czy powinien coś zrobić, czy nie), więc nie jest to zbyt duża pomoc, z wyjątkiem może bardziej czytelnego kodu. A także brak możliwości rozszerzenia dla użytkowników twojego hipotetycznego frameworka.Czasami powinieneś użyć tylko jeśli / to. Jest to prosty kod, który jest łatwy do odczytania.
Dwa kluczowe problemy z prostym kodem „jeśli / to” polegają na tym, że może on naruszać zasadę otwartego zamknięcia . Jeśli kiedykolwiek będziesz musiał wejść i dodać lub zmienić warunek, modyfikujesz ten kod. Jeśli spodziewasz się, że będziesz mieć więcej warunków, po prostu dodanie nowej strategii jest prostsze / czystsze / mniej prawdopodobne do zerwania.
Innym problemem jest sprzężenie. Dzięki zastosowaniu if / then wszystkie implementacje są powiązane z tą implementacją, co utrudnia ich zmianę w przyszłości. Używając strategii, jedynym sprzężeniem jest interfejs strategii.
źródło
Strategia jest przydatna, gdy
if/then
warunki są oparte na typach , jak wyjaśniono w http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.htmlWarunki sprawdzania typu zwykle nie mają dużej złożoności cyklicznej, więc nie powiedziałbym, że Strategia poprawia rzeczy koniecznie tam.
Główny powód strategii wyjaśniono w książce GoF str. 316, która wprowadziła wzór:
Jak wspomniano w innych odpowiedziach, przy odpowiednim zastosowaniu wzorzec strategii pozwala na dodawanie nowych rozszerzeń (konkretnych strategii) bez konieczności modyfikowania reszty kodu. Jest to tak zwana zasada otwartego i zamkniętego lub zasada wariacji chronionych . Oczywiście nadal musisz zakodować nową konkretną strategię, a kod klienta musi rozpoznać strategie jako wtyczki (nie jest to trywialne).
W przypadku
if/then
warunkowej konieczna jest zmiana kodu klasy zawierającej logikę warunkową. Jak wspomniano w innych odpowiedziach, czasami jest to OK, gdy nie chcesz dodać złożoności do obsługi dodawania nowych funkcji (wtyczek) bez ponownej kompilacji.źródło
To właśnie największa korzyść ze wzorca strategicznego. Brak warunków.
Chcesz, aby twoje klasy / metody / funkcje były jak najprostsze i najkrótsze. Krótki kod jest bardzo łatwy do przetestowania i bardzo łatwy do odczytania.
Warunki (
if
/elseif
/else
) powodują, że twoje klasy / metody / funkcje są długie, ponieważ zwykle kod, do któregotrue
odnosi się jedna decyzja, różni się od tej, w której ocenia się decyzjęfalse
.Kolejną wielką zaletą wzorca strategii jest to, że można go ponownie wykorzystać w całym projekcie.
Korzystając ze wzorca projektowania strategii, bardzo prawdopodobne jest, że masz jakiś kontener IoC, z którego uzyskujesz pożądaną implementację interfejsu, być może za pomocą
getById(int id)
metody, w którejid
mógłby to być element wyliczający.Oznacza to, że wdrożenie jest tylko w jednym miejscu kodu.
Jeśli chcesz dodać więcej implementacji, dodajesz nową implementację do
getById
metody, a ta zmiana znajduje odzwierciedlenie wszędzie w kodzie, w którym ją wywołujesz.Z
if
/elseif
/else
nie można tego zrobić. Dodając nową implementację, musisz dodać nowyelseif
blok i zrobić to wszędzie tam, gdzie implementacje były używane, albo możesz skończyć z niepoprawnym kodem, ponieważ zapomniałeś dodać implementację do jego struktury.W moim przykładzie
id
może to być zmienna zapełniana na podstawie danych wejściowych użytkownika. Jeśli użytkownik kliknie przycisk A, toid = 2
jeśli kliknie przycisk B, toid = 8
.Ze względu na inną
id
wartość inna implementacja interfejsu jest uzyskiwana z kontenera IoC, a kod wykonuje różne operacje.źródło
if
/elseif
/else
. Tak jak poprzednio, tylko w innym miejscu.id
zmiennej wgetById
metodzie, który zwróciłby konkretną implementację. Za każdym razem, gdy potrzebujesz implementacji interfejsu, poprosisz kontener IoC o dostarczenie go do ciebie.getSortByEnumType(SortEnum type)
zwracająca implementacjęSort
interfejsu, metodagetSortType
zwracającaSortEnum
zmienną i przyjmująca kolekcję jako parametr, agetSortByEnumType
metoda ponownie zawierałaby przełączniktype
parametru zwracający ci poprawny algorytm sortowania. Jeśli trzeba było dodać nowy algorytm sortowania, wystarczy edytować wyliczenie i jedną metodę. I jesteś gotowy.Wzorzec strategii pozwala oddzielić algorytmy (szczegóły) od logiki biznesowej (zasady wysokiego poziomu). Te dwie rzeczy są nie tylko mylące w czytaniu po zmieszaniu, ale mają również bardzo różne powody do zmiany.
Istnieje również istotny czynnik skalowalności pracy zespołowej. Wyobraź sobie duży zespół programistów, w którym wiele osób pracuje nad tym pakietem księgowym. Jeśli wszystkie algorytmy podatkowe należą do klasy lub modułu TaxPayer , konflikty scalania stają się prawdopodobne. Konflikty scalania są czasochłonne i podatne na błędy. Tym razem drenaż produktywności zmniejsza wydajność zespołu, a błędy wprowadzone przez złe łączą wiarygodność szkód z klientami.
Algorytm, który zmienia się w czasie wykonywania, to taki, którego zachowanie zależy od konfiguracji lub kontekstu. If / następnie zbliżyć się w miejscu nie skutecznie umożliwić ten ponieważ wiąże przeładunku istniejących aktywnie wykorzystywane klas. Dzięki Wzorcowi Strategii obiekty strategiczne implementujące każdy algorytm można konstruować przy użyciu. W rezultacie zmiany tych algorytmów (poprawki lub ulepszenia) mogą zostać wprowadzone i ponownie załadowane w czasie wykonywania. Takie podejście można zastosować w celu zapewnienia ciągłej dostępności i wydań zerowych przestojów.
źródło
Nie ma w tym nic złego
if/else
. W wielu przypadkachif/else
jest to najprostszy i najbardziej czytelny sposób wyrażania logiki. Podejście, które opisujesz, jest więc w wielu przypadkach całkowicie poprawne. (Jest również doskonale testowalny, więc nie stanowi to problemu).Ale są pewne szczególne przypadki, w których wzorzec strategii może poprawić utrzymanie całego kodu. Na przykład:
Aby wzorzec strategii miał sens, interfejs między podstawową logiką a algorytmami obliczania podatków powinien być bardziej stabilny niż poszczególne elementy. Jeśli równie prawdopodobne jest, że zmiana wymagań spowoduje zmianę interfejsu, wówczas wzorzec strategii może faktycznie stanowić zobowiązanie.
Wszystko sprowadza się do tego, czy „algorytmy obliczania podatków” można czysto oddzielić od podstawowej logiki, która je wywołuje. Wzór strategii ma pewne narzuty w porównaniu do
if/else
, więc będziesz musiał zdecydować indywidualnie, czy inwestycja jest tego warta.źródło