Programowanie, wbrew obiegowym opiniom, nie jest aż takie trudne. Wypuszczenie nawet prostych aplikacji mobilnych czy stron nie wymaga dzisiaj dużych nakładów sił. Największym problemem jest napisanie takiego kodu, który będzie łatwo rozszerzalny i będzie miał dla nas sens nawet wtedy, gdy spojrzymy na niego za kilka lat. Ładny kod to nie tylko dobra stylistyka, ale także wzorce projektowe. W ostatnim tekście opisałem stosunkowo mało znany wzorzec null object (zobacz wpis). Dziś z kolei chciałbym się zająć dużo bardziej popularnym rozwiązaniem, czyli tzw. wzorcem strategii.   Wzorzec strategii Niejednokrotnie, tworząc oprogramowanie, musimy być przygotowani do obsłużenia sytuacji, w której kontekst zmienia się dynamicznie. W pewnych okolicznościach program może działać według jednego schematu, w innych zupełnie inaczej. Można by nawet powiedzieć, że ta sama aplikacja może mieć różne strategie działania. Takie warianty muszą być obsłużone niezależnie. Po uruchomieniu programu nie do końca wiadomo, który z nich zostanie użyty. Może to zależeć np. od parametrów wejściowych wprowadzonych na formatce czy też w linii komend.   Aplikacja musi więc obsługiwać rodzinę różnych algorytmów, które mogą posłużyć do rozwiązywania problemów z określonej grupy. Takie algorytmy powinny działać wymiennie — tzn. że da się je wpiąć w to samo miejsce w kodzie i wywoływać odpowiednio w zależności od danych wejściowych.   Być może opis zabrzmiał odrobinę zawile, ale przykład praktyczny pokaże, że wzorzec strategii rozwiązuje standardowe problemy, na które możemy się natknąć w kodzie źródłowym każdego dnia.   Przykład problemu Wyobraźmy sobie, że musimy napisać aplikację, która na bazie danych wprowadzonych przez użytkownika powinna dokonywać wyceny określonego problemu. Na potrzeby przykładu wymyśliłem trzy możliwe warianty wyceny:  

  • ostrożna — mnożymy szacunkową liczbę godzin razy dwa;
  • normalna — brak zmian;
  • agresywna — dzielimy szacunkową liczbę godzin przez dwa.

  Dostępne warianty są opisane za pomocą enumeracji. Użytkownik oprócz wybrania rodzaju wyceny musi jeszcze wprowadzić szacunkową liczbę godzin. Poniżej przykład prostej aplikacji: [sourcecode language="csharp"] public enum EvaluationType { Careful = 0, Normal, Agressive } public class TestProgram { public double Calculate(EvaluationType evaluationType, double hours) { double evaluation = 0; switch(evaluationType) { case EvaluationType.Careful: evaluation = hours * 2; break; case EvaluationType.Agressive: evaluation = hours * 0.5; break; case EvaluationType.Normal: default: evaluation = hours; break; } return evaluation; } } [/sourcecode] Program właściwie składa się z tylko jednej klasy, co oczywiście w kwestii potencjalnego rozwoju niesie ze sobą kilka problemów:

  • zastosowanie switcha, który nie jest do końca czytelny;
  • cały kod znajdujący się w jednej klasie — jawne pogwałcenie kilku zasad SOLID (zobacz wpisy z serii);
  • sporo problemów w przypadku potencjalnego rozwoju kodu dla któregokolwiek z algorytmów.

  Jak to można naprawić? Oczywiście wprowadzając w życie wzorzec strategii.   Refaktoryzacja do wzorca strategii  Podstawowym założeniem programistycznym wzorca strategii jest wprowadzenie abstrakcji (w tym przypadku interfejsu), który będzie definiował metodę wykorzystywaną do obliczeń. Każdy typ wyceny otrzyma swoją dedykowaną klasę, która zaimplementuje wspomniany wyżej interfejs.   Dodatkową atrakcją tego przykładu będzie słownik, który pozwoli na całkowite usunięcie switcha i znaczne uproszczenie klasy testowego programu.   Jedynym elementem, który pozostanie z poprzedniego przykładu, jest enumeracja. Poniżej kod: [sourcecode language="csharp"] public interface IEvaluationStrategy { double Evaluate(double hours); } public class CarefulEvaluationStrategy : IEvaluationStrategy { public double Evaluate(double hours) { return hours * 2; } } public class NormalEvaluationStrategy : IEvaluationStrategy { public double Evaluate(double hours) { return hours; } } public class AggresiveEvaluationStrategy : IEvaluationStrategy { public double Evaluate(double hours) { return hours*0.5; } } public class StrategyPatternProgram { private readonly Dictionary strategies = new Dictionary { {EvaluationType.Careful, new CarefulEvaluationStrategy()}, {EvaluationType.Normal, new NormalEvaluationStrategy()}, {EvaluationType.Agressive, new AggresiveEvaluationStrategy()} }; public double Calculate(EvaluationType evaluationType, double hours) { return this.strategies[evaluationType].Evaluate(hours); } } [/sourcecode] Zwieńczeniem przykładu będzie kod aplikacji konsolowej, który uruchomi stare i nowe rozwiązanie: [sourcecode language="csharp"] public class Program { public static void Main(string[] args) { EvaluationType evaluation = EvaluationType.Careful; double hours = 172.5; // Old way TestProgram testProgram = new TestProgram(); Console.WriteLine($"Test program evaluation: {testProgram.Calculate(evaluation, hours)}"); // Strategy pattern StrategyPatternProgram testStrategyProgram = new StrategyPatternProgram(); Console.WriteLine($"Test strategy program evaluation: {testStrategyProgram.Calculate(evaluation, hours)}"); Console.Read(); } } [/sourcecode]   Oczywiście wprowadzenie wzorca strategii w tak prostym przykładzie znacznie wydłużyło cały kod, ale jednocześnie stał się on dużo bardziej elastyczny i czytelny. W rzeczywistych przypadkach wzorzec strategii powinien być oczywiście wprowadzony z głową. Jak to się często mówi — nie powinniśmy strzelać do muchy z armaty, a także nie obawiać się refaktoryzacji. Inna sprawa to pisanie kodu w taki sposób, by w ogóle nadawał się do refaktoryzacji, ale to już temat na inny wpis…  

Jerzy Piechowiak

Altcontroldelete.pl