Wzorce projektowe Strategia (Strategy)

We wpisie postaram się wyjaśnić i zastosować w praktyce jeden z najpopularniejszych wzorców projektowych Startegia (Strategy )  czasami także znana pod nazwą Policy. Wzorzec strategia ze względu na wersalność jest używany w większości projektów nad którymi pracowałem. W telegraficznym skrócie wzorzec umożliwia nam wykorzystanie różnych algorytmów do wykonania tej samej rzeczy w zależności od kontekstu w jakim będzie wykorzystany.


Poniżej kilka podpunktów definiujących nam wzorzec strategia (Strategy):

  • Pozwala nam zdefiniować grupę algorytmów, w której w każdym z elementów możemy zhermetyzować indywidualną implementację algorytmu i wybrać odpowiedni dynamicznie
  • Wzorzec pozwala nam na wykonanie różnych algorytmów rozwiązujących ten sam problem niezależnie od klienta, który z niej korzysta.
  • Strategia przechowuje abstrakcję wykonania algorytmu w interfejsie, a klasy implementujące ją szczegóły jego realizacji

W skrócie wzorzec strategia (Strategy) umożliwia klientowi rozwiązanie problemu poprzez wybranie sposobu jego rozwiązania ad-hoc bez znajomości szczegółów rozwiązania.

Jak już jesteśmy na bardzo formalnym etapie poniżej diagramik UML obrazujący wzorzec:

Wzorzec strategia (Strategy)
Diagram UML wzorzec strategia (Strategy)

Co tutaj mamy:

  • Client wykonawca algorytmu
  • Interfejs IStrategy, abstrakcja naszego algorytmu, klient wie tylko o jego istnieniu, i tylko z niego korzysta do wykonania algorytmu,
  • Klasy StrategyA i StrategyB, które implementują powyższy interfejs, są to konkretne wersje naszego algorytmu

Kiedy z niej najlepiej skorzystać z wzorca strategia (Strategy)?

  • Wtedy, gdy mamy w powiązanych ze sobą klasach niewielkie różnice w logice,
  • Potrzebujemy kilka wersji algorytmu,
  • Wykorzystywane algorytmy potrzebują dostępu do danych, których nie należy udostępniać klientowi,
  • Chcemy aby zachowanie naszego kodu było zdefiniowane podczas jego użytkowania,
  • Mamy dużo instrukcji warunkowych Case lub If

Przykład wykorzystania:

Obliczanie podatku od wynagrodzenia

Obecnie w przypadku umowy o pracę mamy dwa sposoby rozliczenia z podatku dochodowego. Poprzez skalę i poprzez podatek liniowy. Co z tym idzie mamy drobne różnice w sposobie implementacji algorytmu do obliczenia podatku dochodowego. Klient korzystający z naszej funkcjonalności chce mieć tylko możliwość wyboru sposobu opodatkowania w trakcie działania programu. Nic prostszego, powyższy kontekst idealnie pasuje nam do wzorca strategii (Strategy). Potrzebujemy jedynie interfejsu, dwóch implementacji algorytmu i wola gotowe. To zabieramy się do kodu.

Zacznijmy od zdefiniowania abstrakcji naszego algorytmu (na diagramie UML interfejsu IStrategy)

Zdefiniowaliśmy sobie interfejs ITaxCalculators który posiada tylko jedną metodę Calculate wykorzystywaną do obliczenia podatku. To z tego interfejsu będzie korzystał nasz klient, i to będzie tylko jedno miejsce jego styku z naszymi algorytmami liczącymi podatek dochodowy.

Zabieramy się do implementacji naszych algorytmów, zgodnie z wymaganiami potrzebujemy tylko dwóch. Jednego do obliczeń podatków ze skali podatkowej i drugiego liniowego.

Implementacja dla podatku liniowego 😉

Co tutaj mamy. Nasza klasa LinearCalculatorStrategy implementuje naszą abstrakcję algorytmu w tym przypadku ITaxCalculatorStrategy. W konstruktorze wstrzykujemy serwis, który zwróci nam aktualne oprocentowanie w przypadku podatku liniowego. No kwintesencje algorytmu sposób obliczenia naszego podatku 😉

Pozostał drugi algorytm dla skali podatkowej:

Uff troche kodu jest. Ale spokojnie niczym nie różni się od poprzedniego 😉 Przecież oblicza tylko podatek dochodowy… Jak porzednio mamy konkretny algorytm GeneralCalculatorStrategy implementujący naszą abstrakcję algorytmu ITaxCalculatorStrategy. No i jedną metodę obliczającą podatek dochodowy (plus kilka pomocniczych 🙂 nie będę tu wnikał w szczegóły implementacji sposobu obliczenia podatku dla skali).

Ok ale brakuje nam jeszcze naszego sławnego klienta.

Mamy tu klasę TaxCalculator, która reprezentuje naszego klienta oraz jego abstrakcję ITaxCalculator.  Dzięki takiemu zabiegowi operujemy na abstrakcjach zgodnie z założeniami Dependency Injection. Jeszcze jedna rzecz, warta wyjaśnienia to wykorzystanie IComponentContext. Jest to interfejs dostarczony nam przez kontener dependency injection Autofac. To za  pomocą niego możemy wybrać implementację naszego algorytmu w trakcie działania programu.

Jeszcze dla dopełnienie  przykładu rejestracja naszych elementów w autofac:

I to wszystko. Nic skomplikowanego prawda?

A teraz dodatek.

Dzięki wykorzystaniu wzorca strategii w naszym zadaniu spełniliśmy kilka zasad SOLID 😉

  1. Single Rsponsibiliti Principle: każda zaimplementowana przez nas klasa odpowiada za wykonanie tylko jednej rzeczy. Najlepszy przykład to nasze algorytmy.
  2. Open/Close Principle: Zawsze możemy dodać nowa implementację algorytmu, lub w łatwy sposób zastąpić istniejący bez impaktu na pozostałe elementy naszej aplikacji.
  3. Dependency inversion Principle: Nasze komponenty wyższego rzędu jak TaxCalculator nie są zależne od komponentów niższego rzędu oraz wszystko zależnie jest od abstrakcji a nie od konkretnej implementacji.

Pełną implementację przykładu można zobaczyć w repo : https://github.com/erloon/SF.git

Przewiń do góry