Opis
Wzorzec projektowy Command (Polecenie) należy do grupy wzorców behawioralnych. Pozwala zamknąć całe żądanie wykonania konkretnej czynności w osobnym obiekcie. Przez to łatwo go dostosować za pomocą przekazywanych parametrów, a większość poleceń jest też łatwa do kolejkowania, czy nawet wycofania.
Komenda w rozumieniu klasycznych wzorców projektowych jest czymś innym, niż komenda w popularnym podejściu CQRS. Chociaż koncepty te mają wiele wspólnego to główną ich różnicą jest fakt, że polecenie w CQRS to zwykłe DTO. Najpierw trafia do szyny, która deleguje go do osobnej klasy (zwanej handlerem) odpowiedzialnej za jego obsługę. Za to bohater dzisiejszego wpisu posiada własną metodę execute()
, którą można wywołać bezpośrednio, chociaż zazwyczaj implementuje się tak zwanego Invokera.
Problem i rozwiązanie
Akcja realizująca to samo albo prawie identyczne zadanie w wielu różnych miejscach – w aplikacjach to norma. Przekładając na widok użytkownika, te same przyciski w wielu miejscach. W najgorszym scenariuszu kod jest powielony i obsłużone są one osobno – więc w przypadku zmiany trzeba przeszukać całą aplikację.
Inny problem to pomieszanie między warstwami i wadliwa komunikacja między nimi. Obiekty wiedzą za dużo o innych i mieszają między sobą odpowiedzialności, a w dodatku są od siebie zależne tam gdzie nie powinny.
Wzorzec projektowy Polecenie pozwala na separację odpowiedzialności i enkapsulację. Klasa Invokera nie ma pojęcia za co odpowiedzialna jest dana komenda. Za zadanie ma tylko ją obsłużyć, co najczęściej wiąże się z jej odpaleniem. Czasem jednak może ją zaprowadzić do kolejki albo śledzić jej historię, tym samym pozwalając na przykład na jej wycofanie. Odpowiedzialnością tej klasy jest więc delegowanie – nie powinna wiedzieć, co realizuje dana komenda.
Za to polecenie nie musi wiedzieć w jakim kontekście zostanie uruchomione. Realizuje konkretne zadanie, na żądanie z dowolnego miejsca w programie. Jest też obiektem, który można łatwo parametryzować, więc niewielka różnica między wywołaniem jest w większości przypadków trywialna w obsłużeniu.
Można też tworzyć obszerniejsze komendy zestawiając kilka mniejszych. Przykładowo da się osobno w aplikacji wygenerować dokument (1 komenda), zmienić jego status na „do wysłania” (2 komenda) i pobrać na dysk (3 komenda). To wszystko można zamknąć w jednej dużej komendzie umożliwiając użytkownikowi jednym kliknięciem odpalenie zgrupowanych akcji.
Plusy i minusy
Polecenie wprowadza warstwę pośrednią generującą dodatkową złożoność. Tę warstwę stanowi Invoker, który jest kolejną zależnością w klasach, gdzie trzeba obsłużyć komendę. Mimo wszystko da się je też wykonywać samodzielnie i traktować je na zasadzie niewielkich serwisów, chociaż z Invokera można zrobić większy pożytek. Szczególnie, że jako dodatkowa warstwa rozluźnia powiązanie nadawca-odbiorca.
Jeśli chodzi o plusy, ten wzorzec projektowy żyje w zgodzie z pierwszymi zasadami SOLID, czyli Single Responsibility i Open/Closed. Czyni więc kod bardziej czytelnym oraz łatwym w utrzymaniu i rozwoju. Odpowiednia enkapsulacja zachowania oraz możliwość dostosowania żądania za pomocą parametrów, powodują że komendy są reużywalne. Kolejnymi zaletami Command Pattern jest możliwość ich kolejkowania, a także zachowania historii wykonanych poleceń, co pozwoli na przykład na cofnięcie zmian.
Przykładowa implementacja w PHP
Przykład prezentuje uproszczoną implementację reakcji na daną zawartość. Na przykład post w mediach społecznościowych. Każda reakcja to osobna komenda uruchamiana przez Invokera, który w tym przykładzie jest właśnie odpowiedzialny tylko za to.
Invokera można jednak rozbudować o historię wykonanych komend, czy kolejkowanie – wtedy jest z niego większy pożytek. Zauważcie, że odbiorcą każdej komendy może być zupełnie inny obiekt. Akurat w tym przypadku jest ten sam, ale konstruktor komendy i jej zależności nie mają nic wspólnego z interfejsem.
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Command; interface CommandInterface { public function execute(): void; }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Command; class CommandInvoker { private CommandInterface $command; public function setCommand(CommandInterface $command): void { $this->command = $command; } public function invoke(): void { $this->command->execute(); } }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Command; class Like implements CommandInterface { private Author $author; private InteractiveInterface $interactive; public function __construct(Author $author, InteractiveInterface $interactive) { $this->author = $author; $this->interactive = $interactive; } public function execute(): void { $this->interactive->like($this->author->getId()); } }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Command; class Wow implements CommandInterface { private Author $author; private InteractiveInterface $interactive; public function __construct(Author $author, InteractiveInterface $interactive) { $this->author = $author; $this->interactive = $interactive; } public function execute(): void { $this->interactive->wow($this->author->getId()); } }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Command; interface InteractiveInterface { public function like(string $authorId): void; public function wow(string $authorId): void; }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Command; class Post implements InteractiveInterface { public const LIKE_SLUG = 'like'; public const WOW_SLUG = 'wow'; private array $reactionStatistics; public function __construct() { $this->reactionStatistics = []; } public function like(string $authorId): void { if ($this->shouldUndo($authorId, self::LIKE_SLUG)) { unset($this->reactionStatistics[$authorId]); return; } $this->reactionStatistics[$authorId] = self::LIKE_SLUG; } public function wow(string $authorId): void { if ($this->shouldUndo($authorId, self::WOW_SLUG)) { unset($this->reactionStatistics[$authorId]); return; } $this->reactionStatistics[$authorId] = self::WOW_SLUG; } public function countLikes(): int { $sums = array_count_values($this->reactionStatistics); return $sums[self::LIKE_SLUG] ?? 0; } public function countWows(): int { $sums = array_count_values($this->reactionStatistics); return $sums[self::WOW_SLUG] ?? 0; } private function shouldUndo(string $authorId, string $reactionSlug): bool { return !empty($this->reactionStatistics[$authorId]) && ($this->reactionStatistics[$authorId] === $reactionSlug); } }
<?php declare(strict_types=1); namespace DesignPatterns\Behavioral\Command; class Author { private string $id; public function __construct() { $this->id = uniqid(); } public function getId(): string { return $this->id; } }
<?php declare(strict_types=1); namespace Behavioral\Command\Test; use DesignPatterns\Behavioral\Command\CommandInterface; use DesignPatterns\Behavioral\Command\CommandInvoker; use PHPUnit\Framework\TestCase; final class CommandInvokerTest extends TestCase { public function testCanInvokeCommand(): void { $command = $this->createMock(CommandInterface::class); $command ->expects($this->once()) ->method('execute'); $invoker = new CommandInvoker(); $invoker->setCommand($command); $invoker->invoke(); } }
<?php declare(strict_types=1); namespace Behavioral\Command\Test; use DesignPatterns\Behavioral\Command\Author; use DesignPatterns\Behavioral\Command\Like; use DesignPatterns\Behavioral\Command\Post; use PHPUnit\Framework\TestCase; final class LikeTest extends TestCase { public function testCanReactLike(): void { $interactive = new Post(); $command = new Like(new Author(), $interactive); $command->execute(); $this->assertSame(1, $interactive->countLikes()); $command->execute(); $this->assertSame(0, $interactive->countLikes()); } }
<?php declare(strict_types=1); namespace Behavioral\Command\Test; use DesignPatterns\Behavioral\Command\Author; use DesignPatterns\Behavioral\Command\Post; use DesignPatterns\Behavioral\Command\Wow; use PHPUnit\Framework\TestCase; final class WowTest extends TestCase { public function testCanReactWow(): void { $interactive = new Post(); $command = new Wow(new Author(), $interactive); $command->execute(); $this->assertSame(1, $interactive->countWows()); $command->execute(); $this->assertSame(0, $interactive->countWows()); } }
Command – podsumowanie
Przyznam, że nie często spotykam ten wzorzec projektowy w kodzie w takiej klasycznej postaci. CQRS skradł całe show. Mimo wszystko jest to ciekawy sposób na jednostronną komunikację nadawca-odbiorca żądania. Wydaje mi się, że w innych technologiach jest bardziej popularny, ale mogę się mylić. Po prostu specyfika aplikacji webowych sprawia, że niekoniecznie czerpie się z niego pełnymi garściami.
Odpowiedz