Przejdź do głównej zawartości

[OOP] Na czym polega wzorzec projektowy strategia?


Wzorzec projektowy strategia (ang. strategy) pozwala na zdefiniowanie grupy algorytmów danego typu, które będą mogły być stosowane wymiennie przez klienta.

Strategia należy do grupy wzorców czynnościowych. Więcej o podziale wzorców można przeczytać tutaj [under construction].

Zanim jednak opowiemy sobie więcej o tym wzorcu, rozważmy prostą aplikację będąca interfejsem do bazy danych przechowującej informacje o psach. A wersji 1.0 aplikacja ta ma wyświetlać po prostu listę psów - informację o ich rasie, rozmiarze i zastosowaniu. Zasiadasz do klawiatury.

Zaczynasz od zdefiniowania prostej klasy reprezentującej psa i jego właściwości:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
    class Dog
    {
        public Dog(string breed, Size size, Usage usage)
        {
            Breed = breed;
            Size = size;
            Usage = usage;
        }

        internal string Breed { get; }
        internal Size Size { get; }
        internal Usage Usage { get; }

        public override string ToString()
        {
            return $"{Breed}, rozmiar {Size.Name()}, zastosowanie: {Usage.Name()}";
        }
    }

    enum Size
    {
        Small,
        Medium,
        Big
    }

    static class SizeExtensions
    {
        public static string Name(this Size size)
        {
            switch (size)
            {
                case Size.Small:
                    return "Mały";
                case Size.Medium:
                    return "Średni";
                case Size.Big:
                    return "Duży";
                default:
                    throw new System.Exception("Invalid size");
            }
        }
    }

    internal enum Usage
    {
        Pet,
        Hunting,
        Racing
    }

    static class UsageExtensions
    {
        public static string Name(this Usage size)
        {
            switch (size)
            {
                case Usage.Pet:
                    return "Do towarzystwa";
                case Usage.Hunting:
                    return "Myśliwski";
                case Usage.Racing:
                    return "Wyścigowy";
                default:
                    throw new System.Exception("Invalid usage");
            }
        }
    }

Następnie piszesz właściwy program:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;
using System.Collections.Generic;

namespace Strategy
{
    class Program
    {
        static void Main(string[] args)
        {
            var dogs = new List<Dog>
            {
                new Dog("Ogar", Size.Big, Usage.Hunting),
                new Dog("Mastiff", Size.Big, Usage.Hunting),
                new Dog("York", Size.Small, Usage.Pet),
                new Dog("Dog niemiecki", Size.Big, Usage.Pet),
                new Dog("Chart", Size.Medium, Usage.Racing)
            };

            foreach(var dog in dogs)
            {
                Console.WriteLine(dog);
            }

            Console.ReadKey();
        }
    }
}

Aplikacja działa wyśmienicie i z zachwytem podziwiasz, jak wyświetla dane o psach.

Ogar, rozmiar Duzy, zastosowanie: Mysliwski
Mastiff, rozmiar Duzy, zastosowanie: Mysliwski
York, rozmiar Maly, zastosowanie: Do towarzystwa
Dog niemiecki, rozmiar Duzy, zastosowanie: Do towarzystwa
Chart, rozmiar Sredni, zastosowanie: Wyscigowy

Jednak tydzień później twój manager mówi ci, że nowy klient - lokalne koło łowieckie - zażyczył sobie dodanie możliwości wyświetlenia tylko psów myśliwskich. Po kilku dniach wytężonej pracy twoim oczom ukazuje się kod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System;
using System.Collections.Generic;
using System.Linq;

namespace Strategy
{
    class Program
    {
        static void Main(string[] args)
        {
            var dogs = new List<Dog>
            {
                new Dog("Ogar", Size.Big, Usage.Hunting),
                new Dog("Mastiff", Size.Big, Usage.Hunting),
                new Dog("York", Size.Small, Usage.Pet),
                new Dog("Dog niemiecki", Size.Big, Usage.Pet),
                new Dog("Chart", Size.Medium, Usage.Racing)
            };

            Console.WriteLine("Pokazać wszytskie, czy tylko myśliwskie? (Wpisz w/m)");
            var answer = Console.ReadKey();

            var dogsToBeShown = GetDogsToBeShown(answer.Key, dogs);

            Console.WriteLine();
            foreach (var dog in dogsToBeShown)
            {
                Console.WriteLine(dog);
            }   

            Console.ReadKey();
        }

        private static IEnumerable<Dog> GetDogsToBeShown(ConsoleKey key, List<Dog> dogs)
        {
            if (key == ConsoleKey.M)
            {
                return dogs.Where(d => d.Usage == Usage.Hunting);
            }
            return dogs;
        }
    }
}

Wygląda na to, że wszystko działa:

Pokazac wszytskie, czy tylko mysliwskie? (Wpisz w/m)
m
Ogar, rozmiar Duzy, zastosowanie: Mysliwski
Mastiff, rozmiar Duzy, zastosowanie: Mysliwski

Niestety, twój manager ma kolejne wymaganie. Kolejny klient - koło gospodyń wiejskich - pilnie potrzebuje funkcjonalności wyświetlania tylko małych psów do towarzystwa. Przełykasz ślinę i zasiadasz do pracy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Collections.Generic;
using System.Linq;

namespace Strategy
{
    class Program
    {
        static void Main(string[] args)
        {
            var dogs = new List<Dog>
            {
                new Dog("Ogar", Size.Big, Usage.Hunting),
                new Dog("Mastiff", Size.Big, Usage.Hunting),
                new Dog("York", Size.Small, Usage.Pet),
                new Dog("Dog niemiecki", Size.Big, Usage.Pet),
                new Dog("Chart", Size.Medium, Usage.Racing)
            };

            Console.WriteLine("Pokazać wszystkie, tylko myśliwskie, a może tylko małe psy do towarzystwa? (Wpisz w/m/t)");
            var answer = Console.ReadKey();

            var dogsToBeShown = GetDogsToBeShown(answer.Key, dogs);

            Console.WriteLine();
            foreach (var dog in dogsToBeShown)
            {
                Console.WriteLine(dog);
            }   

            Console.ReadKey();
        }

        private static IEnumerable<Dog> GetDogsToBeShown(ConsoleKey key, List<Dog> dogs)
        {
            if (key == ConsoleKey.M)
            {
                return dogs.Where(d => d.Usage == Usage.Hunting);
            }
            if (key == ConsoleKey.T)
            {
                return dogs.Where(d => d.Usage == Usage.Pet && d.Size == Size.Small);
            }
            return dogs;
        }
    }
}

Twój kolega pyta cię, czy słyszałeś kiedyś o zasadach SOLID, zwłaszcza o Open-Closed Principle (poczytać o niej można tutaj [under construction]). Zbywasz go, bo przecież twój program działa poprawnie:

Pokazac wszystkie, tylko mysliwskie, a moze tylko male psy do towarzystwa? (Wpis
z w/m/t)
t
York, rozmiar Maly, zastosowanie: Do towarzystwa

Kolejnym wymaganiem jest, by istniała możliwość pokazania psów dużych ras, których nazwy zaczynają się na literę M (twój manager robi się nerwowy, kiedy pytasz, kto potrzebuje takiej funkcjonalności). Zasiadasz do napisania kolejnego ifa, niestety koledzy z zespołu zaczynają wysyłać w twoim kierunku groźby karalne. Po długiej dyskusji postanawiacie zastosować wzorzec projektowy strategy.

Wygląda na to, że, wymaganiem, które najczęściej się zmienia, jest strategia filtrowania psów. Dlatego chcemy zamknąć tę strategię (lub innymi słowy: algorytm, metodę, sposób) w odpowiednim interfejsie. Interfejs ten nazwijmy IFilteringStrategy. Klasa dokonująca właściwego filtrowania będzie klientem strategii. W tym przykładzie nazwiemy ją Filterer. Klasa Filterer będzie przechowywać w sobie referencję do obiektu klasy implementującej IFilteringStrategy. Dzięki temu, kiedy pojawi się potrzeba dodania nowego sposobu filtrowania psów, po prostu dodamy nową implementację interfejsu IFilteringStrategy, i podstawimy tę implementację do pola klasy Filterer.

Dodając nową implementację, a nie modyfikując istniejący kod, będziemy respektować Open-Closed Principle.

Tyle teorii. Zacznijmy od zdefiniowania interfejsu IFilteringStrategy:

1
2
3
4
5
6
7
namespace Strategy
{
    internal interface IFilteringStrategy<T>
    {
        bool ShallBeIncluded(T item);
    }
}

Interfejs ten zawiera jedną metodę, którą uczyniłam metodą generyczną. Dzięki temu będziemy mogli użyć tego samego interfejsu, gdy zajdzie potrzeba filtrowania kotów, świnek morskich lub skomplikowanych transakcji finansowych.

Metoda ShallBeIncluded będzie decydować, czy w danej strategii filtrowania obiekt ma zostać dołączony do przefiltrowanej listy, czy nie. By to lepiej zrozumieć, przyjrzyjmy się przykładowym implementacjom tego interfejsu.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace Strategy
{
    internal class HuntingFilteringStrategy : IFilteringStrategy<Dog>
    {
        public bool ShallBeIncluded(Dog dog)
        {
            return dog.Usage == Usage.Hunting;
        }
    }

    internal class SmallPetFilteringStrategy : IFilteringStrategy<Dog>
    {
        public bool ShallBeIncluded(Dog dog)
        {
            return dog.Usage == Usage.Pet && dog.Size == Size.Small;
        }
    }

    internal class BigBreedStartingWithLetterMFilteringStrategy : IFilteringStrategy<Dog>
    {
        public bool ShallBeIncluded(Dog dog)
        {
            return dog.Breed.StartsWith("M", System.StringComparison.InvariantCultureIgnoreCase) && dog.Size == Size.Big;
        }
    }
}

Zerknijmy teraz do klasy Filterer, będącej klientem strategii:


using System;
using System.Collections.Generic;
using System.Linq;

namespace Strategy
{
    internal class Filterer<T>
    {
        protected List<T> _items;

        private IFilteringStrategy<T> _filteringStrategy { get; set; }

        public Filterer(List<T> items, IFilteringStrategy<T> filteringStrategy)
        {
            _items = items;
            _filteringStrategy = filteringStrategy;
        }

        internal virtual void PrintAll()
        {
            foreach (var item in _items.Where(i => _filteringStrategy.ShallBeIncluded(i)))
            {
                Console.WriteLine(item);
            }
            Console.WriteLine();
        }
    }
}

W linijce 21 dzieje się cała magia. Filetrer nie wie według jakich kryteriów przefiltrować elementy listy. Deleguje tę odpowiedzialność do obiektu strategii. Jaka jest konkretna implementacja w ogóle nas tu nie obchodzi. Gdy pojawi się nowa metoda filtrowania, po prostu przekażemy inny obiekt do konstruktora Filterera. Nic poza tym się nie zmieni.

Kto zatem ma podjąć decyzję, jaka strategia zostanie wybrana? Robi to oczywiście użytkownik aplikacji, wybierając odpowiednią opcję z interfejsu. Stwórzmy specjalną klasę, która "przetłumaczy" opcję na odpowiedni obiekt (np. opcję "m", wybieraną gdy chcemy wyświetlić tylko psy myśliwskie, przetłumaczy na obiekt typu HuntingFilteringStrategy).

Klasą tą będzie DogsFilteringStrategyFactory. Mamy tu do czynienia ze wzorcem projektowym fabryka, o którym więcej poczytać można tutaj [under construction].

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;

namespace Strategy
{
    class DogsFilteringStrategyFactory
    {
        public IFilteringStrategy<Dog> GetStragety(ConsoleKey key)
        {
            if (key == ConsoleKey.M)
            {
                return new HuntingFilteringStrategy();
            }
            if (key == ConsoleKey.T)
            {
                return new SmallPetFilteringStrategy();
            }
            if (key == ConsoleKey.B)
            {
                return new BigBreedStartingWithLetterMFilteringStrategy();
            }
            throw new ArgumentException();
        }
    }
}

Zobaczmy, jak teraz wygląda główna część programu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;
using System.Collections.Generic;

namespace Strategy
{

    class Program
    {
        static void Main(string[] args)
        {
            var dogs = new List<Dog>
            {
                new Dog("Ogar", Size.Big, Usage.Hunting),
                new Dog("Mastiff", Size.Big, Usage.Hunting),
                new Dog("York", Size.Small, Usage.Pet),
                new Dog("Dog niemiecki", Size.Big, Usage.Pet),
                new Dog("Chart", Size.Medium, Usage.Racing)
            };

            Console.WriteLine("Wybierz opcję:");
            Console.WriteLine("m - psy myśliwskie");
            Console.WriteLine("t - małe psy do towarzystwa");
            Console.WriteLine("b - duże psy ras zaczynających się na literę M");

            var answer = Console.ReadKey();
            Console.WriteLine();

            var filteringStrategy = new DogsFilteringStrategyFactory().GetStragety(answer.Key);

            var dogsFilterer = new Filterer<Dog>(dogs, filteringStrategy);
            dogsFilterer.PrintAll();

            Console.ReadKey();
        }
    }
}

Wygląda na to, że wszystko działa:

Wybierz opcje:
m - psy mysliwskie
t - male psy dotowarzystwa
b - duze psy ras zaczynajacych sie na litere M
b
Mastiff, rozmiar Duzy, zastosowanie: Mysliwski

Kod niewątpliwie wygląda bardziej elegancko:
  • metody filtrowania zostały zamknięte w odpowiednich klasach
  • wybór metody na podstawie inputu użytkownika stał się jedyną odpowiedzialnością nowej klasy DogsFilteringStrategyFactory
  • dodanie nowych metod filtrowania nie wpłynie na istniejący kod aplikacji
Rozważmy, co teraz zmieni się, gdy nasz manager zażyczy sobie dodania nowej metody filtrowania psów:
  • dodamy nową implementację interfejsu IFilteringStrategy<Dog>
  • dodamy nowe mapowanie w DogsFilteringStrategyFactory
  • dodamy nową literę jaką może wpisać w konsolę użytkownik
Nie będziemy zmieniać istniejącego kodu. Będziemy tylko dodawać nowy kod. Zasada Open-Closed Principle zostanie zachowana.

Kod tego, jak i innych wzorców, można znaleźć pod tym linkiem [under construction].

Komentarze