Wzorzec Command - okładka

Wzorzec projektowy Command (Polecenie)

Opublikowano Kategorie Czysty kod, TypeScriptCzas czytania 4min

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

Wzorzec Command - diagram UML

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.

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

Źródła i materiały dodatkowe

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Zapisując się na mój mailing, otrzymasz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Powiadom o
guest

0 komentarzy
Inline Feedbacks
View all comments