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.
Dodaj komentarz