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
Prześlij komentarz