Wzorzec dekorator

Strukturalny wzorzec projektowy dekorator, to lekarstwo na problemy z mnogością różnych podtypów. Pozwala on na dynamiczne generowanie kolejnych typów obiektów bazujących na pierwotnym typie, bez konieczności deklarowania nowych, dedykowanych klas.


Wzorzec dekorator, co to?

Wzorzec projektowy dekorator (decorator pattern) jest strukturalnym wzorcem projektowym, który czerpie wielkimi garściami z zasady composition over inheritance (kompozycja zamiast dziedziczenia). Pozwala on na dynamiczne nadbudowywanie obiektów o nowe zachowania, ale w obrębie interfejsu bazowego. Daje to wiele możliwości w zakresie dynamicznej kreacji obiektów. 

Jak i kiedy używać?

Większość implementacji tego wzorca zakłada utworzenie klasy dekorator pod wybrane zadania dekorujące, która przechowuje obiekt dekorowany, a także klas konkretnych dekoratorów służących udekorowaniu docelową funkcjonalnością, które rozszerzają bazowy typ. 

Użycie tego wzorca ma najwięcej sensu, jeżeli mamy do czynienia z modelami w hierarchii i pewnymi powiązaniami pomiędzy nimi. Oznacza to, że jeżeli z Twoich klas można utworzyć różne wariacje ich samych, dekorując je dodatkowymi zachowaniami i bazują one na sobie, to wybór wzorca dekorator może okazać się dla Ciebie korzystny.

Problem, który rozwiązuje decorator pattern

Wyobraźmy sobie, że dostarczamy system umożliwiający odbywanie spotkań online. Mamy wiele planów w różnych cenach, które pozwalają na odbywanie spotkań z innymi funkcjonalnościami. Podstawowe spotkanie udostępnia jedynie możliwość rozmowy głosowej. W ofercie mamy jednak spotkania z możliwością rozmów video, z white boardem, z dekorowaniem tła obrazu kamery, z udostępnianiem ekranu i wieloma innymi.

Klient sam może skomponować rozwiązanie, które jest mu potrzebne (i za które będzie płacił), więc zachodzi wariacja w stylu: klasa podstawowa (spotkanie głosowe) x wszystko, np. spotkanie audio & video, spotkanie audio z możliwością screen share, spotkanie audio & video z możliwością screen share, spotkanie audio z white board oraz screen share i tak dalej… Widzisz ten problem w kodzie? 

interface MeetingPlanInterface {
  billableMonthlyPrice(): Money;

  features(): MeetingFeature[];

  name(): string;
}

class VoiceMeetingPlan implements MeetingPlanInterface {
  billableMonthlyPrice = (): Money => new Money(0, Currency.PLN);

  features = (): MeetingFeature[] => [MeetingFeature.voiceMeeting];

  name = (): string => 'Voice meetings';
}

class VoiceAndVideoMeetingPlan implements MeetingPlanInterface {
  billableMonthlyPrice = (): Money => new Money(9.99, Currency.PLN);

  features = (): MeetingFeature[] => [MeetingFeature.voice, MeetingFeature.video];

  name = (): string => 'Voice meetings + video';
}

class VoiceWithScreenShareMeetingPlan implements MeetingPlanInterface {}

class VoiceAndVideoWithScreenShareMeetingPlan implements MeetingPlanInterface {}

// ...

Wzorzec dekorator przykład

Powyższy problem możemy rozwiązać stosując decorator pattern. Wprowadźmy dodatkowe klasy, o których wspomniałem wcześniej. Poniżej widzimy klasę PlanDetailsDecorator, która implementuje ten sam interfejs, co klasa planu podstawowego. Mamy także konkretne klasy dekoratorów w postaci klas rozszerzających dekorator bazowy i dodających „swoje trzy grosze” do implementacji.

class PlanDetailsDecorator implements MeetingPlanInterface {
  constructor(
    protected readonly plan: MeetingPlanInterface
  ) {}

  billableMonthlyPrice = (): Money => this.plan.billableMonthlyPrice();

  features = (): MeetingFeature[] => this.plan.features();

  name = (): string => this.plan.name();
}

class VideoMeet extends PlanDetailsDecorator {
  billableMonthlyPrice = (): Money => this.plan.billableMonthlyPrice()
    .add(9.99, Currency.PLN);

  features = (): MeetingFeature[] => 
    [...this.plan.features(), MeetingFeature.video];

  name = (): string => `${this.plan.name()} + video`;
}

class ScreenShareMeet extends PlanDetailsDecorator {
  billableMonthlyPrice = (): Money => this.plan.billableMonthlyPrice()
    .add(2, Currency.PLN);

  features = (): MeetingFeature[] => 
    [...this.plan.features(), MeetingFeature.screenShare];

  name = (): string => `${this.plan.name()} + screen share`;
}
Co dał nam taki zabieg? Spójrz poniżej, możemy teraz w locie zredefiniować istniejące zakresy planów o nowe funkcjonalności dynamicznie, bez konieczności tworzenia nowych klas dla poszczególnych typów, jak miało to miejsce wcześniej.
 
const freePlan = new VoiceMeetingPlan();

let silverPlan = new VoiceMeetingPlan();
silverPlan = new VideoMeet(silverPlan);

let goldPlan = new VoiceMeetingPlan();
goldPlan = new VideoMeet(goldPlan);
goldPlan = new ScreenShareMeet(goldPlan);

let platinumPlan = new VoiceMeetingPlan();
platinumPlan = new VideoMeet(platinumPlan);
platinumPlan = new ScreenShareMeet(platinumPlan);
platinumPlan = new WhiteboardMeet(platinumPlan);

Podsumowanie




Polecane wpisy:

Wzorzec adapter

Strukturalny wzorzec projektowy adapter to spore udogodnienie w walce z systemami legacy. Pozwala użyć niepasującego interfejsu w innym. Dzięki temu zabiegowi pomimo, iż nie posiadamy spójnej abstrakcji możemy zastosować pożądaną logikę.

Sprawdź ten wpis

Wzorzec budowniczy (builder)

Wzorzec projektowy budowniczy służy do dynamicznej budowy obiektów. Pozwala na kreowanie różnych typów obiektu w zależności od przesłanych wymagań. Dzięki temu posiadamy centralny punkt odpowiedzialny jedynie za tworzenie wariacji danego typu.

Sprawdź ten wpis

Wzorzec metoda wytwórcza (factory metod)

Jeżeli masz do czynienia z tworzeniem różnych obiektów bazujących na predefiniowanym modelu, to powinieneś/aś skorzystać z metody wytwórczej. Kreacyjny wzorzec projektowy factory method pozwala być fancy w tworzeniu obiektów.

Sprawdź ten wpis

SOLID z przykładami w TypeScript

SOLID (Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle, Dependency inversion principle), czyli pięć zasad programowania obiektowego, które każdy powinien przestrzegać.

Sprawdź ten wpis

Jak zostać architektem oprogramowania?

Aby projektować dobrą architekturę oprogramowania trzeba najpierw zaznajomić się z jej przesłankami, rodzajami, a także sposobami wdrażania. Architektura nie jest przecież czymś stałym, co wszędzie implementowane jest w jednakowy sposób.

Sprawdź ten wpis

Autor wpisu:

Gabriel Ślawski

Fanatyk czystego i prostego kodu. Zwolennik podejść DDD oraz Modular Monolith. Na codzień pracuje jako programista i architekt. Po godzinach spełnia się w projektach open source, udziela się na blogach oraz czyta książki o kosmosie i astrofizyce.