Wzorzec Command - okładka

Wzorzec projektowy Command ( Polecenie )

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 na 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.

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().

Wzorzec Command - diagram UML

Natomiast przykładowa implementacja w TypeScript mogłaby wyglądać właśnie tak:


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.

Rozbudowany wzorzec command - Diagram UML

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 w oparciu o wzorzec 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();
		}
	}
}

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.

Źródła i materiały dodatkowe: