Zalety wzorca strategii

15

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?

Armon Safai
źródło
2
Czy to zadanie domowe? Jeśli tak, lepiej powiedzieć to z góry.
Fuhrmanator
2
@Fuhrmanator no it isnt
Armon Safai

Odpowiedzi:

20

Po pierwsze, duże skupiska if/elsebloków nieł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/elseskoń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:

  • Na przykład definiujesz interfejs, TaxCalculationa twoja struktura akceptuje instancje tego typu do obliczania podatków
  • Użytkownik frameworka tworzy klasę, która implementuje ten interfejs i przekazuje go do frameworka, umożliwiając w ten sposób wykonanie części obliczeń

Nie 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/elsebloki są „bezimienny”, w najlepszym przypadku tylko komentuje i komentarze mogą kłamać. Ponadto, według mojego osobistego gustu, posiadanie więcej niż 3 if/elseblokó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” ).

scriptin
źródło
1
if/elsebloki również obniżają czytelność kodu. Jeśli chodzi o wzorzec strategii, warto wspomnieć o IMO.
Maciej Chałapuk
1
Testowalność jest głównym powodem. (Większość) Każda gałąź twojego kodu powinna zostać przetestowana. Im więcej ifmasz, 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, wiele ifstwierdzeń 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.
Greg Burghardt
Dziękuję za Twoją odpowiedź. Dlaczego więc nie mogę używać osobnych metod dla różnych algorytmów w tej samej klasie?
Armon Safai,
Możesz, ale nadal potrzebujesz kilku if/elseblokó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.
scriptin
1
Czy możesz wyjaśnić, dlaczego łatwiej jest testować? Przykład refaktoryzacji instrukcji case (lub if / then) na metodę polimorficzną (podstawa strategii) jest dość łatwy do przetestowania. refactoring.com/catalog/replaceConditionalWithPolymorphism.html Jeśli znam wszystkie warunki do testowania, piszę test dla każdego. Jeśli mam strategie, muszę utworzyć instancję i wykonać jedną dla każdej. W jaki sposób łatwiej jest przetestować podejście strategiczne? Nie mówimy o złożonych zagnieżdżonych ifach, gdy zmienisz strategię.
Fuhrmanator,
5

Dlaczego warto stosować wzorzec strategii, jeśli można po prostu napisać kod w przypadkach „jeśli / to”?

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.

Telastyn
źródło
co jest złego w modyfikowaniu kodu w kodzie if / then? czy nie musiałbyś również modyfikować kodu we wzorcu strategii, jeśli zdecydujesz się zmienić sposób działania jednego z algorytmów?
Armon Safai,
@armonsafai - jeśli zmodyfikujesz strategię, wystarczy ją przetestować. Jeśli zmodyfikujesz wszystkie algorytmy, musisz przetestować wszystkie algorytmy. Co gorsza, jeśli dodasz nową strategię, wystarczy ją przetestować. Jeśli dodasz nowy warunek, musisz przetestować wszystkie warunki warunkowe.
Telastyn
4

Strategia jest przydatna, gdy if/thenwarunki są oparte na typach , jak wyjaśniono w http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html

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

Użyj wzorca strategii, gdy

...

  • klasa definiuje wiele zachowań, które w swoich operacjach pojawiają się jako wiele instrukcji warunkowych. Zamiast wielu warunków warunkowych przenieś powiązane gałęzie warunkowe do własnej klasy strategii.

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/thenwarunkowej 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.

Fuhrmanator
źródło
3

[...] czy możesz po prostu napisać kod w przypadkach, gdy / a?

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órego trueodnosi 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órej idmó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 getByIdmetody, a ta zmiana znajduje odzwierciedlenie wszędzie w kodzie, w którym ją wywołujesz.

Z if/ elseif/ elsenie można tego zrobić. Dodając nową implementację, musisz dodać nowy elseifblok 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.


Co to znaczy, że algorytm zmienia się w czasie wykonywania?

W moim przykładzie idmoże to być zmienna zapełniana na podstawie danych wejściowych użytkownika. Jeśli użytkownik kliknie przycisk A, to id = 2jeśli kliknie przycisk B, to id = 8.

Ze względu na inną idwartość inna implementacja interfejsu jest uzyskiwana z kontenera IoC, a kod wykonuje różne operacje.

Andy
źródło
Dziękuję za Twoją odpowiedź. Dlaczego więc nie mogę używać osobnych metod dla różnych algorytmów w tej samej klasie?
Armon Safai,
@ArmonSafai Czy oddzielne metody naprawdę rozwiązałyby cokolwiek? Nie wydaje mi się Przenosisz problem z jednego miejsca do drugiego, a decyzja, którą metodę wywołać, zostanie podjęta na podstawie wyniku warunku. Ponownie stan if/ elseif/ else. Tak jak poprzednio, tylko w innym miejscu.
Andy,
Więc przypadki, jeśli / to byłyby w porządku, prawda? Czy nie musielibyście również stosować przypadków, jeśli / i, głównie dla wzorca strategii?
Armon Safai,
1
@ArmonSafai Nie, nie zrobiłbyś tego. Miałbyś przełącznik na idzmiennej w getByIdmetodzie, który zwróciłby konkretną implementację. Za każdym razem, gdy potrzebujesz implementacji interfejsu, poprosisz kontener IoC o dostarczenie go do ciebie.
Andy,
1
@ArmonSafai Równie dobrze mogłaby istnieć metoda getSortByEnumType(SortEnum type)zwracająca implementację Sortinterfejsu, metoda getSortTypezwracająca SortEnumzmienną i przyjmująca kolekcję jako parametr, a getSortByEnumTypemetoda ponownie zawierałaby przełącznik typeparametru zwracający ci poprawny algorytm sortowania. Jeśli trzeba było dodać nowy algorytm sortowania, wystarczy edytować wyliczenie i jedną metodę. I jesteś gotowy.
Andy,
2

Dlaczego warto stosować wzorzec strategii, jeśli można po prostu napisać kod w przypadkach „jeśli / to”?

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.

Co to znaczy, że algorytm zmienia się w czasie wykonywania?

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.

Alain O'Dea
źródło
1

Nie ma w tym nic złego if/else. W wielu przypadkach if/elsejest 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:

  • Jeżeli określone algorytmy obliczania podatków mogą się zmieniać niezależnie od siebie i od podstawowej logiki. W takim przypadku dobrze byłoby je rozdzielić na odrębne klasy, ponieważ zmiany zostaną zlokalizowane.
  • Jeśli w przyszłości mogą zostać dodane nowe algorytmy, bez zmiany logiki rdzenia.
  • Jeśli przyczyna różnicy między tymi dwoma algorytmami wpływa również na inne części kodu. Załóżmy, że wybrałeś dwa algorytmy na podstawie przedziału dochodów podatnika. Jeśli ten przedział dochodów powoduje również wybranie różnych gałęzi innych miejsc w kodzie, wówczas łatwiej jest utworzyć strategię odpowiadającą temu przedziałowi dochodów i zadzwonić w razie potrzeby, zamiast mieć wiele rozrzuconych po kodzie gałęzi if / else.

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.

JacquesB
źródło