Wzorzec projektowy Command (Polecenie) uważam za jeden z najprostszych do zrozumienia. Pisząc ten artykuł, wzorowałem się na interpretacji przedstawionej przez Roberta C. Martina w książce pt. “Agile Programowanie zwinne zasady wzorce i praktyki zwinnego wytwarzania oprogramowania w C#”. Szukając dodatkowych informacji na temat tego wzorca, napotkałem również wersje znacznie bardziej rozbudowane. Niemniej jednak do zrozumienia zasady działania i celu tego wzorca, interpretacja z wcześniej wymienionej książki powinna być wystarczająca.
Ten wpis jest kolejnym wpisem z serii o wzorcach projektowych. Jeśli chcesz poznać inne wzorce projektowe lub dowiedzieć się czym są wzorce, to koniecznie sprawdź mój wpis o wzorcach projektowych.
Struktura
Patrząc na strukturę wzorca, wydaje się on być wręcz banalny. Cała magia wzorca polega na stworzeniu interfejsu z jedną publiczną metodą execute()
.
Natomiast przykładowa implementacja w TypeScript mogłaby wyglądać następująco.
interface ICommand {
execute(): void;
}
class ExampleClass implements ICommand {
public execute(): void {
// implementation
}
}
Bardziej rozbudowane implementacje wzorca projektowego Command mogą oferować dodatkowe funkcje takie jak walidacja danych czy możliwość cofania operacji.
Zastosowanie
Największą zaletą tego wzorca jest to, że w momencie wywoływania metody execute()
nie muszę znać żadnych szczegółów implementacyjnych reszty klasy. Cała logika jest enkapsulowana w powyższej metodzie.
Przykładem takiego zachowania może być proces parzenia kawy w ekspresie. Aby zaparzyć kawę, potrzebujemy sprawdzić, czy w pojemniku znajduje się woda oraz, czy ekspres uzyskał odpowiednią temperaturę. Obie te czynności powinny być obsłużone w metodzie execute()
. Co prawda, przy dwóch czynnościach siła tego wzorca może nie być szczególnie widoczna, jednak zaczyna ona być zauważalna, gdy do naszego ekspresu chcielibyśmy dodać np. spieniacz mleka, dodawanie cukru, różne warianty kawy czy automatyczne mielenie ziaren.
interface ICommand {
execute(): void;
}
class CoffeeMachine implements ICommand {
public execute( params: object ): void {
// implementation
}
}
const coffeeMaker: CoffeeMaker = new CoffeeMaker();
coffeeMaker.execute( params );
Poprzez parametry metody execute()
mogę zdefiniować, jaką konkretnie kawę chcę uzyskać, a całą logikę procesu tworzenia kawy zamknąć we wcześniej wspomnianej metodzie. Dodatkowo implementując mechanizm walidacji, jestem w stanie sprawdzić, czy ktoś nie próbuje zrobić np. kawy mrożonej, której zaimplementowany ekspres nie jest w stanie przygotować.
Co prawda, w tym przypadku nie jestem w stanie zaimplementować metody undo()
, w końcu nie da się “odzrobić” kawy. Świetnym przykładem aplikacji do zaimplementowania metody undo()
może być edytor graficzny lub edytor tekstowy, gdzie cofanie zmian jest na porządku dziennym.
Mając do dyspozycji stos wykonań metody execute()
przez obiekty oraz metodę undo()
jestem w stanie przeglądać, oraz odtworzyć historię zmian w np. pliku graficznym oraz cofnąć się do dowolnego punktu w przeszłości. Jest to kolejna z głównych zalet tego wzorca projektowego.
Zagnieżdżanie
Korzystając z wcześniej wspomnianego przykładu ekspresu do kawy, warto pamiętać, że każdy z komponentów ekspresu tj. czujniki, czy podajniki również może być zbudowany na podstawie wzorca Command. Oznacza to, że każdy komponent ekspresu również może wystawiać tylko jedną metodę, przez co niskopoziomowe szczegóły implementacyjne tj. komunikacja z fizycznymi czujnikami i odczytami z nich są ukryte. Dzięki temu rozbudowa ekspresu o kolejne komponenty staje się całkiem prosta i intuicyjna.
class CoffeeMachine {
public constructor(
private readonly _temperatureSensor: TemperatureSensor,
private readonly _waterSensor: WaterSensor,
private readonly _coffeeDispenser: CoffeeDispenser,
private readonly _milkDispenser: MilkDispenser,
private readonly _sugarDispenser: SugarDispenser,
) {}
public execute( coffeeType: string, withMilk: boolean = false, withSugar: boolean = false): void {
this._temperatureSensor.execute();
this._waterSensor.execute();
this._coffeeDispenser.execute( coffeeType );
if ( withMilk ) {
this._milkDispenser.execute();
}
if ( withSugar ) {
this._sugarDispenser.execute();
}
}
}
Podsumowanie
W tym miejscu zostawię Cię z odrobiną niedosytu, po to, abyś samodzielnie wypróbował/wypróbowała ten wzorzec w praktyce. Zachęcam też do zapoznania się ze źródłami w celu dokładniejszego poznania tematu.
Książkę Design Patterns: Elements of Reusable Object-Oriented Software możesz kupić klikając w TEN link. Kupując z tego linku, wspierasz rozwój bloga.
Zapisz się na mailing i odbierz e-booka
Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.