
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:

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)
using SF.Domain.DTO; namespace SF.Domain.TaxCalculators { public interface ITaxCalculator { decimal Calculate(TaxCalculationContext context); } }
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 😉
public class LinearCalculatorStrategy : ITaxCalculatorStrategy { private readonly ITaxPercentagesService _taxPercentagesService; public LinearCalculatorStrategy(ITaxPercentagesService taxPercentagesService) { _taxPercentagesService = taxPercentagesService; } public decimal Calculate(TaxCalculationContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); var percentage = GetTaxPercentage(); return context.CurrentIncome * percentage; } private decimal GetTaxPercentage() { IncomeTaxThreshold incomeTaxThreshold = _taxPercentagesService.GetLinearRate() ?? throw new DomainException("Tax percentages not found"); return incomeTaxThreshold.Percentage; } }
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:
public class GeneralCalculatorStrategy : ITaxCalculatorStrategy { private readonly ITaxPercentagesService _taxPercentagesService; public GeneralCalculatorStrategy(ITaxPercentagesService taxPercentagesService) { _taxPercentagesService = taxPercentagesService ?? throw new ArgumentNullException(nameof(taxPercentagesService)); } public decimal Calculate(TaxCalculationContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); decimal taxValue = 0; List incomeTaxThresholds = GetIncomeTaxThreshold(); var currentValueIncomeTaxThreshold = GetCurrentValueIncomeTaxThreshold(context, incomeTaxThresholds); if (IsLimitValueMonth(currentValueIncomeTaxThreshold, context)) { IncomeTaxThreshold nextThrashold = GetNextIncomeTaxThreshold(incomeTaxThresholds, currentValueIncomeTaxThreshold); taxValue = CalculateForBorderMonth(context, currentValueIncomeTaxThreshold, nextThrashold); } else { taxValue = CalculateForFullMonth(currentValueIncomeTaxThreshold, context); } return taxValue; } private decimal CalculateForFullMonth(IncomeTaxThreshold currentValueIncomeTaxThreshold, TaxCalculationContext context) { return context.CurrentIncome * currentValueIncomeTaxThreshold.Percentage; } private decimal CalculateForBorderMonth(TaxCalculationContext context, IncomeTaxThreshold currentValueIncomeTaxThreshold, IncomeTaxThreshold nextThrashold) { decimal taxValue = (currentValueIncomeTaxThreshold.ToAmount - context.TotalIncomes) * currentValueIncomeTaxThreshold.Percentage; var remainingValueForNextThreshold =((context.TotalIncomes + context.CurrentIncome) - nextThrashold.FromAmount) * nextThrashold.Percentage; return taxValue + remainingValueForNextThreshold; } private IncomeTaxThreshold GetNextIncomeTaxThreshold(List incomeTaxThresholds, IncomeTaxThreshold currenTaxThreshold) { return incomeTaxThresholds.FirstOrDefault(x => x.ThresholdNumber == (currenTaxThreshold.ThresholdNumber + 1)); } private bool IsLimitValueMonth(IncomeTaxThreshold currentValueIncomeTaxThreshold, TaxCalculationContext context) { var currentIncomesSum = context.TotalIncomes + context.CurrentIncome; return currentIncomesSum > currentValueIncomeTaxThreshold.ToAmount && context.TotalIncomes > currentValueIncomeTaxThreshold.FromAmount; } private List GetIncomeTaxThreshold() { return _taxPercentagesService.GetGeneralIncomeTaxThresholds(); } private IncomeTaxThreshold GetCurrentValueIncomeTaxThreshold(TaxCalculationContext context, List incomeTaxThresholds) { var value = (context.TotalIncomes + context.CurrentIncome); var retval = incomeTaxThresholds.FirstOrDefault(x => x.FromAmount <= context.TotalIncomes && x.ToAmount >= value || x.ToAmount >= context.TotalIncomes); return retval; } }
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.
public class TaxCalculator : ITaxCalculator { private readonly IComponentContext _componentContext; public TaxCalculator(IComponentContext componentContext) { _componentContext = componentContext ?? throw new ArgumentNullException(nameof(componentContext)); } decimal ITaxCalculator.Calculate(TaxCalculationContext context) { ITaxCalculatorStrategy calculator = null; calculator = _componentContext.ResolveKeyed(context.TaxationForm); if (calculator == null) throw new DomainException($"Tax calculator for {context.TaxationForm} not found"); return calculator.Calculate(context); } }
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:
builder.RegisterType() .As() .InstancePerLifetimeScope(); builder.RegisterType() .As() .InstancePerLifetimeScope(); builder.RegisterType() .Keyed(TaxationForm.GENERAL) .InstancePerLifetimeScope(); builder.RegisterType() .Keyed(TaxationForm.LINEAR) .InstancePerLifetimeScope();
I to wszystko. Nic skomplikowanego prawda?
A teraz dodatek.
Dzięki wykorzystaniu wzorca strategii w naszym zadaniu spełniliśmy kilka zasad SOLID 😉
- Single Rsponsibiliti Principle: każda zaimplementowana przez nas klasa odpowiada za wykonanie tylko jednej rzeczy. Najlepszy przykład to nasze algorytmy.
- 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.
- 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
No comment yet, add your voice below!