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

  • Dzięki wzorcowi dekorator możemy dynamicznie dodać nowe zachowania wybranemu obiektowi.
  • Stosuj ten wzorzec tam, gdzie istnieje potrzeba tworzenia modeli rozszerzalnych.
  • Decorator pattern może skutkować posiadaniem dużej ilości małych klas dekorujących – uważaj na to.
Autor wpisu

blog@orbisbit.com

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Sprawdź również
  • CQRS – Command Query Responsibility Segregation

    Command Query Responsibility Segregation czyli CQRS. Jest to wzorzec projektowy, który rozdziela zadania odczytu i zapisu do osobnych modeli. Sprawdź ten wpis, aby dowiedzieć się kiedy i jak z niego skorzystać.

    Zobacz wpis

  • GRASP – kolejny zbiór zasad Clean Code do zapamiętania

    Pewnie większość z Was słyszała o zasadach SOLID. Są one bardzo rozpowszechnione i dosyć często stosowane, ale czy słyszeliście o GRASP? General Responsibility Assignment Software Patterns, to kolejna dawka zasad czystego kodu do zapamiętania.

    Zobacz wpis

  • Wzorzec strategia (strategy pattern)

    Jeżeli masz dość if-ologii w swoim kodzie, to konieczne sprawdź czym jest czynnościowy wzorzec projektowy strategia. Pozwala on mądrze obsługiwać różne scenariusze w procesie i jednocześnie być fancy pod względem zasad SOLID.

    Zobacz wpis