DekoratorWzór.1 Dekorator. Śmiało mogę powiedzieć, że jest to jeden z najważniejszych wzorców projektowych. Można powiedzieć, że jest on prawie częścią każdego systemu, ponieważ nie ma co ukrywać, jest on pożyteczny i użyteczny nawet do dzisiaj.

Dekorator pozwala dodać istniejącej klasie nowe zachowanie.  Nie zmienia on jednak działanie klasy podstawowej. Oznacza to, że spełnia następujące zasady S.O.L.I.D:

 

  • pojedynczej odpowiedzialności
  • zasady otwartej-zamkniętej

Dekorator, jak i jego klasa bazowa mają tylko jedno zadanie. Dekorator pozwala nawet rozbić zachowanie klas, jeśli widzisz, że te rozbicie jest potrzebne. W tym przykładzie to pokażę.

Chciałbyś się zapewne nauczyć tego wzorca. Zamiast tego przyjrzymy się przykładzie Pizzy. Kod jest do pobranie na końcu artykułu.

Wyobraź sobie, że masz następujący zestaw klas.

Decorator Pattern

Mamy klasę abstrakcyjną, która reprezentuje to, czym każda pizza będzie. Każda pizza musi posiadać dwie metody. Jedna z nich informuje nas o tym, ile Pizza będzie kosztować. Druga zwróci nazwę pizzy.

public abstract class Pizza
{
    public abstract double CalculateCost();

    public abstract string GetName();
}

Na chwilę obecną mamy definicję małej pizzy.

public class SmallPizza : Pizza
{
    public override double CalculateCost()
    {
        return 12.00;
    }

    public override string GetName()
    {
        return "Small Pizza";
    }
}

Średniej pizzy.

public class MediumPizza : Pizza
{
    public override double CalculateCost()
    {
        return 25.00;
    }

    public override string GetName()
    {
        return "Medium";
    }
}

…oraz dużej pizzy.

public class LargePizza : Pizza
{
    public override double CalculateCost()
    {
        return 50.00;
    }

    public override string GetName()
    {
        return "Large Pizza";
    }
}

Każda z nich ma swoją osobistą nazwę i cenę. Wszystko wydaje się w porządku, ale do czasu. Otóż na horyzoncie pojawił się cały zestaw innych dodatków do pizzy.

Dodatki do pizzy lekko modyfikują istniejące już mechanizmy naszej pizzy.

Decorator Pattern Visual Studio

Problem jednak polega na tym, że istnieje dużo kombinacji dodatków. Mamy pizzę z szynką lub z pieczarkami. Mamy też pizzę, która zawiera oba te składniki.

Widząc wszystkie te klasy widzimy, że robimy coś źle. Nie możemy mieć klasy do każdego możliwego przypadku. Musimy ten przypadek jakoś utworzyć dynamicznie.

public class LargePizzaWithHamAndCheese : Pizza
{
    public override double CalculateCost()
    {
        return 52.00;
    }

    public override string GetName()
    {
        return "LargePizzaWithHamAndCheese";
    }
}

Duża pizza z szynką i serem kosztuje 52 złote. Co, jeśli jednak zwiększę cenę sera. To znaczy, że muszę zmieniać tę cenę w tylu klasach równocześnie. Dlaczego jednak nie mieć samej klasy, która by reprezentowała tylko ser i wtedy bym ustalał cenę tylko w jednym miejscu.

Gdybyśmy tak mogli dekorować istniejącą pizzę, a potem, jeśli zajdzie potrzeba udekorować ją jeszcze raz kolejnym składnikiem, ale jak to zrobić.

Przejdźmy więc do naszego głównego bohatera dekoratora. Jak widzisz ilość klas znacznie się zmniejszyła.

Decorator Pattern Visual Studio 2

Klasa abstrakcja reprezentująca podstawę Pizzy nie uległa zmianie.

public abstract class Pizza
{
    public abstract double CalculateCost();
    public abstract string GetName();
}

To samo tyczy się wszystkich rozmiarów

public class LargePizza : Pizza
{
    public override double CalculateCost()
    {
        return 50.00;
    }

    public override string GetName()
    {
        return "Large Pizza";
    }
}
public class MediumPizza : Pizza
{
    public override double CalculateCost()
    {
        return 25.00;
    }

    public override string GetName()
    {
        return "Medium";
    }
}
public class SmallPizza : Pizza
{
    public override double CalculateCost()
    {
        return 12.00;
    }

    public override string GetName()
    {
        return "Small Pizza";
    }
}

Tutaj jednak siedzi serce naszego wzorca. Oto klasa implementująca wzorzec dekoratora. Dekorator musi dziedziczyć po klasie abstrakcyjnej lub interfejsie reprezentującej wszystkie możliwe klasy dekorujące.

Dekorator też może być dekorowany dlatego on też dziedziczy.  

Dekorator powinien w sobie posiadać pole dostępne tylko dla klas dziedziczących. To pole reprezentuje poprzednią informację o pizzy.

Jak widzisz metody  odwołują się do tej pizzy z pola. Pole to zostanie uzupełnione w konstruktorze. Czyli definicję pizzy przesyłamy do konstruktora.

Jak zaraz zobaczysz daje to nam możliwość stworzenia stosów różnych dekoratorów, które będą wywoływany kolejne definicje.

public class PizzaDecorator : Pizza
{

    //obiekt, który będzie dekorowany
    protected Pizza _pizza;

    public PizzaDecorator(Pizza pizza)
    {
        _pizza = pizza;
    }
    public override double CalculateCost()
    {
        return _pizza.CalculateCost();
    }

    public override string GetName()
    {
        return _pizza.GetName();
    }
}

Na razie wszystko jeszcze nie jest jasne, bo sam dekorator tylko jest podstawką, na której bazie utworzymy główne bardziej określone mechanizmy dekoratorów.

Oto nasz pierwszy serowy dekorator.

public class CheeseDecorator : PizzaDecorator
{
    public CheeseDecorator(Pizza pizza) 
        : base(pizza)
    {

    }

    public override double CalculateCost()
    {
        return base.CalculateCost() + 2.15;
    }

    public override string GetName()
    {
        return base.GetName() + " Cheese";
    }
}

Dekorator serowy wykonuje metody z poprzednich klas dziedziczących dodając od siebie swoją nazwę oraz cenę.

class Program
{
    static void Main(string[] args)
    {
        Pizza largePizza =  new LargePizza();

        largePizza = new CheeseDecorator(largePizza);

        Console.WriteLine("{0:C2}", largePizza.CalculateCost());

        Console.ReadKey();
    }
}

Użycie dekoratora jest proste. Tworzę sobie dużą pizzę i do dekoratora serowego przekazuję swoją dużą pizzę.

Console WriteLine

Code Decorator

W trakcie wykonywania się programu najpierw uruchomi się dekorator serowy.

Code Decorator 2

Który później odwoła się do dużej pizzy. W wyniku czego otrzymam sumowaną cenę dużej pizzy z serem.

Nic nie stoi na przeszkodzie, aby utworzyć więcej dekoratorów.

Decorator Pattern Visual Studio 4

Jak dekorator pieczarkowy…

public class ChampignonsDecorator : PizzaDecorator
{
    public ChampignonsDecorator(Pizza pizza) 
        : base(pizza)
    {
    }

    public override double CalculateCost()
    {
        return base.CalculateCost() + 6.15;
    }

    public override string GetName()
    {
        return base.GetName() + ", Champignons";
    }
}

…czy szynkowy.

public class HamDecorator : PizzaDecorator
{
    public HamDecorator(Pizza pizza) :
        base(pizza)
    {
    }

    public override double CalculateCost()
    {
        return base.CalculateCost() + 4.15;
    }

    public override string GetName()
    {
        return base.GetName() + ", Ham";
    }
}

Mogę dekorator nakładać na siebie jeden po drugim.

class Program
{
    static void Main(string[] args)
    {
        Pizza largePizza =  new LargePizza();

        largePizza = new CheeseDecorator(largePizza);
        largePizza = new HamDecorator(largePizza);
        largePizza = new ChampignonsDecorator(largePizza);

        Console.WriteLine("{0:C2}", largePizza.CalculateCost());
        Console.WriteLine(largePizza.GetName());

        Console.ReadKey();
    }
}

Jak widzisz otrzymałem sumę działania wszystkich dodanych dekoratorów

Console

Jaka jednak była kolejność wykonywania?

Decorator

Najpierw wykonał się dekorator pieczarkowy. Został on w końcu dodany jako ostatni.

Później klasa bazowa dekoratora wykona kolejny dekorator. A jest nim dekorator szynkowy.

Decorator code 3

Który później w podobny sposób wywoła dekorator serowy.

Decorator code 4

…i tak dalej…i tak dalej…aż dojdzie on do dużej pizzy i zacznie wracać, aby w końcu zwrócić ostateczną cenę pizzy z tymi wszystkimi dodatkami.

Call Stack

Otrzymałem więc pewien stos wywołań metod klas. Warto zaznaczyć, że każda teraz klasa odpowiada tylko za swój cel. Co więcej, możemy dynamicznie tworzyć zachowania, które są ich kombinacją

Mówiłem, że wzorzec dekoratora jest bardzo użyteczny. Przejdźmy jednak do prawdziwego świata.

W ostatnim przykładzie zainstalowałem kontener wstrzykiwania zależności, który pozwoli utworzyć dynamicznie definicję obiektu bardziej profesjonalnie. 

Decorator Pattern Visual Studio 5

Postanowiłem skorzystać z Castle.Windsor, ponieważ jest to najbardziej znany mi kontener wstrzykiwania zależności.

NuGET

Używając kontenera IOC mogę zadeklarować, że definicja dużej pizza będzie udekorowana z góry przez szynkę.

using Castle.MicroKernel.Registration;
using Castle.Windsor;
using DecoratorPattern.Components;
using DecoratorPattern.ConcreteComponents;
using DecoratorPattern.ConcreteDecorator;

namespace DecoratorPattern
{
    public static class  BootStraper
    {
        public static IWindsorContainer Container;

        public static void Boot()
        {
            Container = new WindsorContainer();

            Container.Register(
                Component.For<Pizza>().ImplementedBy<HamDecorator>(),
                Component.For<Pizza>().ImplementedBy<LargePizza>()
                );

            Pizza p = Container.Resolve<Pizza>();
            var name = p.GetName();
        }
    }
}

Jest to prosty program, więc nie robię nic dalej z tymi definicjami zarejestrowanych klas.

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

        BootStraper.Boot();

        Console.ReadKey();
    }
}

Dekorator może też fantastycznie się nadać do opakowywania klas metodami logującymi. Mogę np. informować siebie, co się dzieje przed wykonaniem metody pod spodem oraz po.

public class LoggerPizzaDecorator : PizzaDecorator
{

    public LoggerPizzaDecorator(Pizza pizza) : base(pizza)
    {
    }

    public override double CalculateCost()
    {
        Console.WriteLine("Before CalculateCost");
        var result = base.CalculateCost();
        Console.WriteLine("After CalculateCost " + result);
        return result;
    }

    public override string GetName()
    {
        Console.WriteLine("Before GetName");
        var name =  base.GetName();
        Console.WriteLine("After GetName : " + name);
        return name;
    }
}

Używając kontenera, oto jak mogę dodać tę klasę logującą.

public static void Boot()
{
    Container = new WindsorContainer();

    Container.Register(
        Component.For<Pizza>().ImplementedBy<LoggerPizzaDecorator>(),
        Component.For<Pizza>().ImplementedBy<HamDecorator>(),
        Component.For<Pizza>().ImplementedBy<LargePizza>()

        );

    Pizza p = Container.Resolve<Pizza>();
    var name = p.GetName();
}

Oto działanie logującej klasy.

Console Pizza

Nic nie stoi na przeszkodzie, aby stworzyć prawdziwy stos dekoratorów.

public static void Boot()
{
    Container = new WindsorContainer();

    Container.Register(
        Component.For<Pizza>().ImplementedBy<LoggerPizzaDecorator>(),
        Component.For<Pizza>().ImplementedBy<HamDecorator>(),
        Component.For<Pizza>().ImplementedBy<ChampignonsDecorator>(),
        Component.For<Pizza>().ImplementedBy<CheeseDecorator>(),
        Component.For<Pizza>().ImplementedBy<LargePizza>()

        );

    Pizza p = Container.Resolve<Pizza>();
    var name = p.GetName();
}

Klasa logująca informuje mnie o rezultacie wszystkich dekoratorów, w końcu jest ona na  samym szczycie wywołań.

Console Pizza 2

Jeśli umieszczę klasę logującą niżej, to oczywiście otrzymam mniej informacji.

public static void Boot()
{
    Container = new WindsorContainer();

    Container.Register(

        Component.For<Pizza>().ImplementedBy<HamDecorator>(),
        Component.For<Pizza>().ImplementedBy<ChampignonsDecorator>(),
        Component.For<Pizza>().ImplementedBy<CheeseDecorator>(),
        Component.For<Pizza>().ImplementedBy<LoggerPizzaDecorator>(),
        Component.For<Pizza>().ImplementedBy<LargePizza>()

        );

    Pizza p = Container.Resolve<Pizza>();
    var name = p.GetName();
}

To byłoby na tyle, jeżeli chodzi o działanie dekoratora i jego prawdziwe użycie.

Console Pizza 3

Czy ten wzorzec ma wady? Być może gdybyś stworzył dynamiczny obiekt składający się z 50 dekoratorów, wtedy byś odczuł mały spadek wydajności. Ten problem został jednak już dawno rozwiązany w .Netcie.

Pamiętaj, by skorzystać z tego wzorca, gdy masz dużo klas, których zachowanie się powiela. Może jesteś w stanie je wydzielić i stworzyć do tego oddzielne dekoratory.

Kto wie może musisz coś zrobić przed i po wykonaniu jakieś metody np. logowanie – wtedy dekorator nada się znakomicie.

A jeśli potrzebujesz czegoś jeszcze będziesz zaawansowanego, zainteresuj się programowaniem aspektowym.

Edit z 2022 roku:
Przeniosłem kod na GitHuba: PanNiebieski/DecoratorPatternWithPizza (github.com)