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 to na widok użytkownika – te same przyciski w wielu miejscach. W najgorszym scenariuszu kod jest powielony i obsłużone są one osobno. 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, w miejscach 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 chociażby 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 (pierwsza komenda), zmienić jego status na „do wysłania” (druga komenda) i pobrać na dysk (trzecia 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ę też wykonywać je samodzielnie i traktować je na zasadzie niewielkich serwisów. Tyle, że 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 pojedyncza odpowiedzialność (single responsibility) i otwarte-zamknięte (open-closed). Czyni 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ść. Wystarczy sobie wyobraźić post w mediach społecznościowych. Każda reakcja to osobna komenda uruchamiana przez Invokera
.
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;
final 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;
final class Like implements CommandInterface
{
public function __construct(
private Author $author,
private InteractiveInterface $interactive
) {}
public function execute(): void
{
$this->interactive->like($this->author->getId());
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Command;
final class Wow implements CommandInterface
{
public function __construct(
private Author $author,
private InteractiveInterface $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;
final class Post implements InteractiveInterface
{
public const string LIKE_SLUG = 'like';
public const string WOW_SLUG = 'wow';
public function __construct(
private array $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;
final class Author
{
private string $id;
public function __construct()
{
$this->id = uniqid();
}
public function getId(): string
{
return $this->id;
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\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);
$invoker = new CommandInvoker();
$invoker->setCommand($command);
$command
->expects($this->once())
->method('execute');
$invoker->invoke();
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\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();
self::assertSame(1, $interactive->countLikes());
}
}
<?php
declare(strict_types=1);
namespace DesignPatterns\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();
self::assertSame(1, $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. Jako, że ten wzorzec projektowy doskonale wspiera regułę DRY to sam koncept można też wykorzystywać w innych miejscach. Małe obiekty z parametryzowanymi metodami używane w wielu miejscach są bardzo czytelnym i praktycznym zabiegiem.
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.
fajnie byloby zobaczyc jakis przyklad z wykorzystaniem tego invokera w kodzie