Artykuł

freeimages.com freeimages.com
mar 06 2015
0

Wzorzec strategia - przykładowa implementacja w C#

Jeśli ktoś spytałby mnie o element który kojarzy mi się z programowaniem niezależnie od zastosowanego języka, to odpowiedziałbym prosto - instrukcje warunkowe. Wszelkiej maści IFy i switche pojawiają się w praktycznie każdym szanującym się języku programowania i tak naprawdę trudno sobie wyobrazić jakąkolwiek sensowną aplikację, która by z nich nie korzystała. I choć w teorii dają one wiele dobrego, to w praktyce, przy nieodpowiednim użyciu, mogą w sposób znaczący obniżyć czytelność tworzonego kodu.

Pewnym rozwiązaniem tego problemu są wzorce projektowe, które pokazują jak łatwo można uprościć instrukcje warunkowe, a czasem nawet całkowicie je wyeliminować. Tytułowy wzorzec strategii zalicza się do tego zaszczytnego grona wzorców, które walczą z tego rodzajami problemów. W praktyce jest on bardzo popularny, dlatego też wypadałoby by mieć przynajmniej jakiekolwiek pojęcie o nim. Pora najwyższa na usystematyzowanie swoich wiadomości:-)

Idea

Z założenia wzorzec strategii ma upraszczać instrukcje warunkowe if/switch. Cel ten można osiągnąć stosunkowo łatwo, wystarczy zastosować się do poniższych instrukcji.

Po pierwsze, musimy zidentyfikować czy określony fragment kodu rzeczywiście pasuje do wzorca strategii. Taka sytuacja ma miejsce najczęściej, w momencie gdy określony zestaw danych przetwarzamy na różne sposoby w zależności od pewnego warunku.

Po drugie, musimy ustalić wspólny interfejs, który posłuży nam do budowania klas oferujących konkretne strategie rozwiązania określonego problemu.

Trzeci krok różni się w zależności od podejścia. W niniejszym tekście wykorzystam dodatkową klasę kontekstu.

Ostatnim krokiem może być stworzenie metody wytwórczej, która w zależności od kontekstu utworzy inny obiekt.

Warto wspomnieć, że wzorzec strategii nie usuwa instrukcji warunkowych, aczkolwiek bardzo często je upraszcza, ponieważ cały kod który pierwotnie się w nich znajdował, trafia do dedykowanych klas. Jeśli klasy są proste i nie mają cech wspólnych, możemy skorzystać z interfejsu. W przeciwnym przypadku warto pomyśleć o rozwiązaniu opartym o klasę abstrakcyjną, które pozwoli na reużycie kodu w klasach poszczególnych strategii.

Przykładowy problem

Teoria jest ważna, ale warto byłoby zobrazować wzorzec w praktyce. Spójrzmy na przykładowy, problematyczny program:

enum Operation
{
    Addition = 0,
    Subtraction,
    Multiplication,
    Division
}

class Program
{
    static void Main(string[] args)
    {
        double a = 10;
        double b = 2;
        double result = 0;
        Operation operation = Operation.Addition;

        switch(operation)
        {
            case Operation.Addition:
                result = a + b;
                break;
            case Operation.Subtraction:
                result = a - b;
                break;
            case Operation.Multiplication:
                result = a * b;
                break;
            case Operation.Division:
                result = a / b;
                break;
        }

        Console.WriteLine(result);
        Console.ReadKey();
    }
}

Cały program jest stosunkowy prosty, ale już teraz widać tutaj potencjalne problemy. Po pierwsze - wszystkie operacje wykonujemy wewnątrz switcha. Oczywiście w tym przypadku jest on krótki, ale nie trudno sobie wyobrazić switcha - potworka w którym w każdym case będzie kilkanaście - kilkadziesiąt linii kodu. Po drugie nie mamy tutaj żadnej separacji kodu. Oczywiście w normalnych warunkach nie będziemy tworzyć nowej klasy dla jednej linii kodu, ale w tym przypadku chodzi o sam przykład i zobrazowanie użycia wzorca;-)

Interfejs dla strategii

Nasza strategia jest prosta i nie będzie zawierała współdzielonego kodu, dlatego też skorzystamy z prostego interfejsu:

public interface IStrategy
{
    double Calculate(double a, double b);
}

Klasy strategii

Dopełnieniem interfejsu będą cztery proste klasy strategii, w których zaimplementujemy różne działanie metody Calculate:

public class AdditionStrategy : IStrategy
{
    public double Calculate(double a, double b)
    {
        return a + b;
    }
}

public class SubtractionStrategy : IStrategy
{
    public double Calculate(double a, double b)
    {
        return a - b;
    }
}

public class MultiplicationStrategy : IStrategy
{
    public double Calculate(double a, double b)
    {
        return a * b;
    }
}

public class DivisionStrategy : IStrategy
{
    public double Calculate(double a, double b)
    {
        return a / b;
    }
}

Opcjonalna klasa kontekstu

Opcjonalnym elementem całego rozwiązania jest klasa kontekstu, która sprawi że nasz kod będzie jeszcze bardziej elegancki. Ideą tej klasy jest utworzenie pośredniej konstrukcji pomiędzy klasami strategii, a właściwym wywołaniem całego kodu:

public class Context
{
    private IStrategy strategy = null;

    public Context(IStrategy strategy)
    {
        this.strategy = strategy;
    }

    public double Calculate(double a, double b)
    {
        return this.strategy.Calculate(a, b);
    }
}

Spinamy wszystko w całość

Punktem wyjścia była nasza aplikacja konsolowa, spójrzmy jak będzie ona wyglądać po refaktoringu:

class Program
{
    static void Main(string[] args)
    {
        double a = 10;
        double b = 2;
        Operation operation = Operation.Addition;
        Context context = null;

        switch (operation)
        {
            case Operation.Addition:
                context = new Context(new AdditionStrategy());
                break;
            case Operation.Subtraction:
                context = new Context(new SubtractionStrategy());
                break;
            case Operation.Multiplication:
                context = new Context(new MultiplicationStrategy());
                break;
            case Operation.Division:
                context = new Context(new DivisionStrategy());
                break;
        }

        Console.WriteLine(context.Calculate(a, b));
        Console.ReadKey();
    }
}

Jak widać, zaszło tutaj kilka zmian. Przede wszystkim z wnętrza switcha zniknęły jakiekolwiek operacje - teraz tworzymy tutaj tylko nowy obiekt klasy Context, do którego przekazujemy obiekt odpowiedniej strategii (linie: 13,16,19,22). Samą operację kalkulacji wykonujemy już poza switchem (26). Całe rozwiązanie wydaje się być dużo bardziej czytelne.

Oczywiście tak jak pisałem wcześniej, nie zawsze jest sens na siłę wprowadzać wzorzec projektowy, zwłaszcza w tak prostych rozwiązaniach. Jednak jeśli takiego kodu jest więcej, to refaktoryzacja do wzorca projektowego strategii, będzie krokiem w dobrym kierunku.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus