Przejdź do głównej zawartości

[OOP] Na czym polega wzorzec projektowy dekorator?


Wzorzec projektowy dekorator (ang. decorator) służy do dodawania funkcjonalności do istniejących klas w czasie działania programu.

Dekorator należy do wzorców strukturalnych. Więcej o typach wzorców projektowych można poczytać tutaj [under construction].

Rozważmy następujący problem: naszym zadaniem jest napisanie aplikacji do zarządzania pizzerią. Na samym początku działania pizzerii dostępna jest tylko jedna pizza: margherita, czyli pizza tylko z sosem pomidorowym i serem. Tworzymy klasę, która będzie ją reprezentować.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace Decorator
{
    internal class Pizza
    {
        private const int BASIC_PIZZA_PRICE = 20;

        virtual public string About()
        {
            return "To jest pizza z sosem pomidorowym, serem";
        }

        public virtual int Price()
        {
            return BASIC_PIZZA_PRICE;
        }
    }
}

Z czasem jednak dostępne stają się kolejne pizze. W menu pojawia się do wyboru jeszcze pizza z szynką, i pizza z pieczarkami. Cóż, skoro nowe rodzaje pizzy SĄ rodzajem pizzy, naturalne wydaje się zastosowanie dziedziczenia:

 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
namespace Decorator
{
    internal class Pizza
    {
        private const int BASIC_PIZZA_PRICE = 20;

        public virtual string About()
        {
            return "To jest pizza z sosem pomidorowym, serem";
        }

        public virtual int Price()
        {
            return BASIC_PIZZA_PRICE;
        }
    }

    internal class PizzaWithHam : Pizza
    {
        private const int EXTRA_INGREDIENT_PRICE = 5;

        public override string About()
        {
            return base.About() + ", szynką";
        }

        public override int Price()
        {
            return base.Price() + EXTRA_INGREDIENT_PRICE;
        }
    }


    internal class PizzaWithMushroom : Pizza
    {
        private const int EXTRA_INGREDIENT_PRICE = 3;

        public override string About()
        {
            return base.About() + ", pieczarkami";
        }

        public override int Price()
        {
            return base.Price() + EXTRA_INGREDIENT_PRICE;
        }
    }
}

Zauważyć można tu podobieństwa między klasami PizzaWithHam i PizzaWithMushroom. Przydałby się refactoring, który wyciągnie część wspólną do klasy bazowej.

 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
namespace Decorator
{
    internal class Pizza
    {
        private const int BASIC_PIZZA_PRICE = 20;

        public virtual string About()
        {
            return "To jest pizza z sosem pomidorowym, serem";
        }

        public virtual int Price()
        {
            return BASIC_PIZZA_PRICE;
        }
    }

    internal abstract class PizzaWithExtraIngredient : Pizza
    {
        protected abstract int ExtraIngredientPrice { get; }
        protected abstract string ExtraIngredientName{ get; }

        public override string About()
        {
            return base.About() + ", " + ExtraIngredientName;
        }

        public override int Price()
        {
            return base.Price() + ExtraIngredientPrice;
        }
    }

    internal class PizzaWithHam : PizzaWithExtraIngredient
    {
        protected override int ExtraIngredientPrice => 5;
        protected override string ExtraIngredientName => "szynką";
    }


    internal class PizzaWithMushroom : PizzaWithExtraIngredient
    {
        protected override int ExtraIngredientPrice => 2;
        protected override string ExtraIngredientName => "pieczarkami";
    }
}

Póki co wygląda to jeszcze nie najgorzej. Co najważniejsze, działa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using System;

namespace Decorator
{
    class Program
    {
        static void Main(string[] args)
        {

            var pizzaWithHam = new PizzaWithHam();
            Console.WriteLine(pizzaWithHam.About());
            Console.WriteLine("Kosztuje ona: " + pizzaWithHam.Price());

            Console.ReadKey();
        }
    }
}


To jest pizza z sosem pomidorowym, serem, szynka
Kosztuje ona: 25

Okazuje się jednak, że niektórzy lubią pizzę i z szynką, i z pieczarkami. Co teraz?

1
2
3
4
5
    internal class PizzaWithHamAndMushroom : PizzaWithExtraIngredient
    {
        protected override int ExtraIngredientPrice => 7;
        protected override string ExtraIngredientName => "szynką, pieczarkami";
    }

Nie wygląda to dobrze. Stworzyliśmy potwora, będącego dziwną mutacją między jedną klasą a drugą. Co, gdy zmieni się cena pieczarek? Będziemy musieli dokonać zmian w dwóch klasach. Co gorsza, za każdym razem gdy w menu pojawi się nowy składnik, będziemy musieli stworzyć kolejnego potwora. Brnąc dalej w to rozwiązanie ani się obejrzymy jak stworzymy klasę.

internal class PizzaWithHamAndMushroomAndOnionAndTunaAndOlivesAndAnchois : PizzaWithExtraIngredient

Którą to łatwo będzie pomylić z

internal class PizzaWithHamAndMushroomAndJalapenoAndTunaAndOlivesAndAnchois : PizzaWithExtraIngredient

Chyba teraz jasno widać, że stosowanie dziedziczenia to w tym przypadku "highway to hell".

Na szczęście na pomoc przychodzi nam dekorator.

Zasada tworzenia dekoratora jest dość prosta. Przeróbmy nasz kod krok po kroku:

1) Klasa Pizza pozostaje bez zmian

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace Decorator
{
    internal class Pizza
    {
        private const int BASIC_PIZZA_PRICE = 20;

        public virtual string About()
        {
            return "To jest pizza z sosem pomidorowym, serem";
        }

        public virtual int Price()
        {
            return BASIC_PIZZA_PRICE;
        }
    }
}

2) Tworzymy klasę PizzaDecorator. Będzie to klasa abstrakcyjna, z której dziedziczyć będą konkretne dekoratory. W naszym przypadku będą to PizzaWithHamDecorator i PizzaWithMushroomDecorator. Klasa PizzaDecorator jest trywialna i posiada tylko referencję do obiektu klasy Pizza i konstruktor. Jednak kryje się w niej ważny szczegół: dziedziczy ona z klasy Pizza. A zatem wszędzie, gdzie będziemy używać referencji do obiektu klasy Pizza, będziemy mogli podstawić obiekt klasy PizzaDecorator.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace Decorator
{
    internal abstract class PizzaDecorator : Pizza
    {
        protected Pizza _internalPizza;
        protected PizzaDecorator(Pizza p)
        {
            _internalPizza = p;
        }
    }
}

3) Tworzymy konkretne dekoratory:

 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
namespace Decorator
{
    internal class PizzaWithMushroom : PizzaDecorator
    {
        public PizzaWithMushroom(Pizza p) : base(p) { }

        public override string About()
        {
            return _internalPizza.About() + ", pieczarkami";
        }

        public override int Price()
        {
            return _internalPizza.Price() + 3;
        }
    }

    internal class PizzaWithHam : PizzaDecorator
    {
        public PizzaWithHam(Pizza p) : base(p) { }

        public override string About()
        {
            return _internalPizza.About() + ", szynką";
        }

        public override int Price()
        {
            return _internalPizza.Price() + 5;
        }
    }
}

Jak widzimy, klasy PizzaWithHamDecorator i PizzaWithMushroomDecorator nadpisują metody About i Price. Posługuje się tu referencją do obiektu klasy Pizza, dodając coś od siebie do wyników działania jej metod.

Cała "magia" dekoratora polega na tym, że PizzaDecorator dziedziczy z klasy Pizza. A to oznacza, że pod obiektem _internalPizza, może kryć się obiekt klasy PizzaWithHam lub PizzaWithMushroom.

Spróbujmy się zastanowić, co jak zachowa się taki obiekt:

var pizzaWithHamAndMushroom = new PizzaWithHam(new PizzaWithMushroom(new Pizza()));

Patrząc od zewnątrz:
  • tworzymy obiekt klasy PizzaWithHam, który jako parametr konstuktora przyjmuje obiekt klasy PizzaWithMushroom. Zatem dla obiektu pizzaWithHamAndMushroom pod polem _inernalPizza kryje się obiekt klasy PizzaWithMushroom...
  • ...który to jako parametr konstruktora przyjmuje bazową pizzę. Dla niej więc _internalPizza jest po prostu typu Pizza
Co więc stanie się, kiedy na obiekcie pizzaWithHamAndMushroom zawołamy metodę About? Obiekt klasy PizzaWithHam zawoła taki kod:

1
2
3
4
        public override string About()
        {
            return _internalPizza.About() + ", szynką";
        }

Ale przecież w tym przypadku _internalPizza to obiekt klasy PizzaWithMushroom! A zatem _internalPizza.About() zwróci wynik działania tej metody:

1
2
3
4
        public override string About()
        {
            return _internalPizza.About() + ", pieczarkami";
        }

Jaki jest wynik działania tej metody? Dla PizzaWithMushroom obiekt _internalPizza to zwykła, bazowa pizza. Zwróci ona:

1
2
3
4
        public virtual string About()
        {
            return "To jest pizza z sosem pomidorowym, serem";
        }

Ostatecznie wynik będzie taki, jak się spodziewaliśmy:

1
2
To jest pizza z sosem pomidorowym, serem, pieczarkami, szynka
Kosztuje ona: 28

Dodanie nowego składnika jest teraz tylko kwestią dodania nowego dekoratora.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
namespace Decorator
{
    internal class PizzaWithOnion : PizzaDecorator
    {
        public PizzaWithOnion(Pizza p) : base(p) { }

        public override string About()
        {
            return _internalPizza.About() + ", cebulą";
        }

        public override int Price()
        {
            return _internalPizza.Price() + 2;
        }
    }
}

Pokuśmy się jeszcze o refactoring powtórzeń...


namespace Decorator
{
    internal abstract class PizzaDecorator : Pizza
    {
        protected abstract string IngredientName { get; }
        protected abstract int IngredientPrice { get; }

        protected Pizza _internalPizza;
        protected PizzaDecorator(Pizza p)
        {
            _internalPizza = p;
        }

        public override string About()
        {
            return _internalPizza.About() + ", " + IngredientName;
        }

        public override int Price()
        {
            return _internalPizza.Price() + IngredientPrice;
        }
    }

    internal class PizzaWithHam : PizzaDecorator
    {
        public PizzaWithHam(Pizza p) : base(p) { }

        protected override string IngredientName => "szynką";
        protected override int IngredientPrice => 5;
    }

    internal class PizzaWithOnion : PizzaDecorator
    {
        public PizzaWithOnion(Pizza p) : base(p) { }

        protected override string IngredientName => "cebulą";
        protected override int IngredientPrice => 3;
    }

    internal class PizzaWithMushroom : PizzaDecorator
    {
        public PizzaWithMushroom(Pizza p) : base(p) { }

        protected override string IngredientName => "pieczarkami";
        protected override int IngredientPrice => 2;
    }
}

Możemy teraz puścić wodze fantazji i bez problemu skomponować pizzę z podwójną szynką, pieczarkami i cebulą:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
using System;

namespace Decorator
{
    class Program
    {
        static void Main(string[] args)
        {
            var pizzaAllTheWay = new PizzaWithHam(new PizzaWithHam(new PizzaWithMushroom(new Pizza())));
            Console.WriteLine(pizzaAllTheWay.About());
            Console.WriteLine("Kosztuje ona: " + pizzaAllTheWay.Price());

            Console.ReadKey();
        }
    }
}


To jest pizza z sosem pomidorowym, serem, pieczarkami, szynka, szynka
Kosztuje ona: 32

Kiedy jeszcze dekorator może być pomocny? Wyobraźmy sobie taką sytuację: do przeprowadzania operacji na bazie danych używamy w swoim projekcie interfejsu IRepository implementowanego przez klasę Repository. Wygląda to mniej więcej tak:

    public interface IRepository
    {
        void SaveToDatabase();
    }

    public class Repository : IRepository
    {
        public void SaveToDatabase()
        {
            //Zapisujemy dane do bazy
        }
    }

    class RepositoryClient
    {
        public void DoSomethig(IRepository repo)
        {
            repo.SaveToDatabase();
        }
    }

Jest to, oczywiście, uproszczenie. W prawdziwym przykładzie implementacja Repository mogłaby być zupełnie poza naszym zasięgiem.

Załóżmy, że pojawia się potrzeba, by logować każdy wyjątek, jaki pojawi się w czasie pracy repozytorium. Co teraz? Możemy, oczywiście, rozważać napisanie klasy dziedziczącej z Repository... ale co, jeśli Repository jest sealed? Może się też okazać, że pojawiają się coraz to nowsze wymagania odnośnie Repository... że ma logować wyjątki, tworzyć backupy, sprawdzać poprawność danych itd. Możemy - podobnie jak miało to miejsce z pizzą - "wkopać się" w pisanie coraz to nowych i bardziej skomplikowanych kombinacji poszczególnych funkcjonalności.

Rozwiązaniem jest użycie dekoratora do "owrapowania" starej implementacji repozytorium:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    public class RepositoryLogDecorator : IRepository
    {
        public IRepository _inner;

        public RepositoryLogDecorator(IRepository inner)
        {
            _inner = inner;
        }

        public void SaveToDatabase()
        {
            try
            {
                _inner.SaveToDatabase();
            }
            catch (Exception ex)
            {
                // logujemy  informację o wyjątku
            }
        }
    }

W ten sposób wystarczy podmienić obiekt, jaki przekazujemy do RepositoryClient:

1
2
            var repositoryClient = new RepositoryClient();
            repositoryClient.DoSomethig(new RepositoryLogDecorator(new Repository()));

Podsumujmy zalety dekoratora:

  • możliwość rozszerzania funkcjonalności klas bez ingerencji w istniejący kod
  • możliwość torzenia nowych, złożonych, wielofunkcyjnych obiektów w runtime, zamiast tworzenia kolejnych klas dziedziczących
  • redukcja zależności między klasami - pojedyncza funkcjonalność zostaje zamknięta w klasie niezależnej od innych funkcjonalności

Kod z tego posta, jak i z innych postów o wzorcach projektowych, można pobrać tutaj [under construction].

W poście tym korzystałam z jak zawsze niezawodnego StackOverflow:
  • https://stackoverflow.com/questions/40294622/understanding-decorator-design-pattern-in-c-sharp
  • https://stackoverflow.com/questions/11668150/decorator-pattern-vs-inheritance-with-examples
  • https://stackoverflow.com/questions/40996028/what-are-the-differences-between-decorator-wrapper-and-adapter-patterns

Komentarze