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:

KISS: keep it simple, stupid!

Zasada KISS: "Keep it simple, stupid", może zostać dosłownie przetłumaczona na: "rób to prosto, głupku". Mówi ona o tym, abyśmy tworzyli kod w jak najprostszy i najbardziej czytelny sposób. Już dziś sprawdź, czego się wystrzegać, aby spełniać KISS.

Sprawdź ten wpis

Wzorce projektowe

Wzorce projektowe zostały stworzone po to, aby nie wymyślać przysłowiowego koła na nowo. Znajomość wzorców projektowych i umiejętność ich stosowania pozwala na szybkie rozwiązywanie problemów. Wpis ten radzi, jakie wzorce zastosować u siebie.

Sprawdź ten wpis

Wzorzec singleton

Kolejny kreacyjny wzorzec projektowy omawiany na łamach tego bloga: singleton. Wzorzec dookoła którego narosło wiele mitów i legend. Dziś o tym dlaczego singleton jest antywzorcem, jakie problemy powoduje oraz kiedy warto po niego sięgnąć.

Sprawdź ten wpis

Wzorzec stan (state)

Dziś prezentuję kolejny wzorzec projektowy, który pozwala uniknąć drabinek if-else if. Mowa tutaj o wzorcu state, który idealnie nada się, jeśli posiadasz różne stany w swoim systemie oraz chcesz mieć możliwość płynnego przechodzenia pomiędzy nimi.

Sprawdź ten wpis

Wzorzec pełnomocnik (virtual proxy)

Wzorzec virtual proxy służy do tworzenia obiektów pośrednich, które nadzorują dostęp do obiektów dla których są pełnomocnikami. Dzięki temu, że pełnomocnik i klasa bazowa udostępniają jednakowy interfejs, możemy ukryć w pośredniku dodatkową logikę.

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.