Wprowadzenie

Wzorzec strategii może być traktowany jako wzorzec projektowy reprezentujący zachowanie, który jedocześnie jest prosty w implementacji i łatwy w zrozumieniu. Znajduje on zostosowanie w scenariuszach, w których obiekt będzie odpowiedzialny za wykonywanie wspólnych działań, które będą wybierane z szeregu dostępnych opcji.

Przejdźmy do zrozumienia definicji wzorca strategii, którą możemy odnaleźć w różnych zasobach sieci. "Wzorzec strategii definiuje rodzinę algorytmów, które są odpowiedzialne za wykonanie zadania na kilka różnych sposobów. Algorytmy takie muszą zostać poddane hermetyzacji oraz mogą być zamieniane w trakcie wykonywania programu”. Rozbijmy definicję na trzy części:

  • Rodzina algorytmów - definicja ta mówi nam o tym, iż definiujemy kilka algorytmów, które mają tą samą funkcjonalność (np. sortowanie), ale proces ten wykonują w odmienny sposób;
  • Hermetyzacja - odznacza, iż wzorzec wymusza od nas, aby alogrytmy były umieszczone w różnych klasach. Takie zachowanie pomaga nam w wyborze odpowiedniego algorytmu dla naszego obiektu;
  • Zamienność - zaleta wzorca polega na możliwości wyboru algorytmu w trakcie wykonywania programu oraz przypisania go do naszego obiektu.


Aplikacja

Zaprezentuje teraz przykład, który używa wzorca strategii oraz pozwoli na praktyczne wykorzystanie definicji. Załóżmy, że mamy przygotować aplikację, której zadaniem jest posortowanie różnych typów obiektów. Oznacza to, iż taka aplikacja pozwalałby na posortowanie wszystkich studentów politechniki po ich numerach indeksów, numerów kart miejskich wszystkich pasażerów czy nawet wszystkich mieszkańców danego województwa po ich nazwiskach.

Problem wydaje się dość prosty, ponieważ sprowadza się do jednego, tj, sortowania. Odświeżmy teraz swoją wiedzę ze studiów, kiedy profesorowie uczyli nas o różnych algorytmach. Złożoność różnych typów algorytmów jest pojęciem względnym – bazuje na liczbie oraz typie elementów do posortowania. Odejdźmy od tej wiedzy, załóżmy (poprawnie lub błędnie), że szybkie sortowanie (quick sort) będzie najlepsze do posortowania wszystkich mieszkańców województwa po ich nazwiskach, sortowanie przez scalanie (merge sort) będzie odpowiednie dla numerów indeksów (int) a sortowanie przez kopcowanie (heap sort) zostanie użyte to posotowania numerów kart miejskich. Jest to dobry przykład zaprezentowania sposobu użycia wzorca strategii. Algorytmy zostaną umieszczone w różnych klasach oraz będą wybierane w trakcie wykonywania programu.

W pierwszej kolejności przygotujemy wspólny interfejs dla naszych algorytów sortowania:

interface ISortingStrategy
{
    void Sort<T>(List<T> dataToSort);
}
A następnie algorytmy do sortowania naszych danych:
QuickSort:
class QuickSort : ISortingStrategy
{
    public void Sort<T>(List<T> dataToSort)
    {
        // Logika sortowania szybkiego
    }
}
MergeSort:
class MergeSort : ISortingStrategy
{
    public void Sort<T>(List<T> dataToSort)
    {
        // Logika sortowania przez scalanie
    }
}
HeapSort:
class HeapSort : ISortingStrategy
{
    public void Sort<T>(List<T> dataToSort)
    {
        // Logika sortowania przez kopcowanie
    }
}
A następnie typ wyliczeniowy dla różnych typów danych do posortowania:
namespace StrategyPattern.Enums
{
	public enum ObjectToSort
	{
		NumerAlbumuStudenta,
		NumerKartyMiejskiej,
		NazwiskoMieszkanca
	}
}
Wszystko jest już gotowe. Poniżej przykład pokazujący użycie wzorca wraz ze szczegółowym objaśnieniem:
using System.Collections.Generic;
using StrategyPattern.Enums;
namespace StrategyPattern
{
	class Program
    {
        static void Main(string[] args)
        {
            ISortingStrategy sortingStrategy = null;
            // W pierwszej kolejności sorotwanie po nazwiskach mieszkańców
            List<string> voivodeshipResidence = new List<string> { "ab", "ac", "xa", "td" };
            sortingStrategy = GetSortingOption(ObjectToSort.NazwiskoMieszkanca);
            sortingStrategy.Sort(voivodeshipResidence);
            // W trakcie wykonywania zmiana algorytmu:
            // Posortujemy teraz studentów po numerach indeksów
            List<int> studentNumbers = new List<int> { 123456, 9876543, 345432 };
            sortingStrategy = GetSortingOption(ObjectToSort.NumerAlbumuStudenta);
            sortingStrategy.Sort(studentNumbers);
            // Kolejna zmiana algorytmu
            List<string> cityCartNumbers = new List<string> { "A123456", "B9876543", "C345432" };
            sortingStrategy = GetSortingOption(ObjectToSort.NumerKartyMiejskiej);
            sortingStrategy.Sort(cityCartNumbers);
        }
        private static ISortingStrategy GetSortingOption(ObjectToSort objectToSort)
        {
            ISortingStrategy sortingStrategy = null;
            // w zależności od przekazanych danych użyjemy innego sortowania
            // zmiana algorytmu odbywa się w trakcie wykonywania programu
            switch (objectToSort)
            {
                case ObjectToSort.NumerAlbumuStudenta:
                    sortingStrategy = new MergeSort();
                    break;
                case ObjectToSort.NumerKartyMiejskiej:
                    sortingStrategy = new HeapSort();
                    break;
                case ObjectToSort.NazwiskoMieszkanca:
                    sortingStrategy = new QuickSort();
                    break;
                default:
                    break;
            }
            return sortingStrategy;
        }
    }
}

Obiekt ISortingStrategy decyduje o tym, który algorym ma zostać użyty do wykonania operacji. Wspaniałą rzeczą w takim podejściu jest fakt, iż w przypadku wadliwego/powolnego/nieoptymalnego działania naszego algorytmu wystarczy zmienić jego odwołanie w metodzie GetSortingOption a dzięki temu nie musimy nic zmieniać w kodzie klienta (tj. klasa program). Można dodatkowo przygotować specjalną implemtancję algorytmu działającego na ogromnej ilości danych (np. godziny szczytu w komunikacji miejskiej) a następnie podmienić jego wykonywanie w trakcie działania programu. Na podstawie liczby pasażerów obiekt ISortingStrategy może zmienić wykonywanie algorytmu z HeapSort na HugeDataSort.


Strategy Pattern vs Factory Pattern

W powyższym przykładzie możemy zobaczyć, że wzorzec strategii wygląda jak wzorzec fabryki, ponieważ instancje algorytmów tworzone są w instrukcji wyboru switch. Jednakże cel użycia wzorców jest odmienny. Wzorzec fabryki jest metodą wytwórczą, która skupia się na tworzenie obiektów w aplikacji. Z kolei wzorzec strategii jest wzorcem zachowania, który zajmuje się organizacją obiektów w zależności od ich zachowania. W powyższym przykładzie należy skupić się na tym jak algorytmy zmieniane są w czasie rzeczywistym oraz, że wywodzą się z rodziny algorytmów. Prawdziwe jest stwierdzenie, że na potrzeby powyższego przykładu zostało stworzone coś w rodzaju fabryki, aby utworzyć instancję tych obiektów.