3.2. Command

3.2.1. Scopo

Incapsulare l’invocazione e il disaccoppiamento.

Abbiamo un Invoker e un Receiver. Questo pattern usa il «Command»per delegare le chiamate ai metodi verso il Receiver e mostrare il metodo «execute» unificato. Inoltre l” Invoker sa solo che deve chiamare il metodo «execute» per eseguire il Command del client. Il Receiver è disaccoppiato dall’Invoker.

Il secondo aspetto di questo pattern è che il metodo undo() permette di annullare il metodo execute(). I comandi possono esssere accorpati per creare uno più complesso con pochissimi copia-incolla preferendo la composizione invece dell’ereditarietà.

3.2.2. Esempi

  • Un editor di testo: tutti gli eventi sono Command i quali possono essere annullati, impilati e salvati.

  • Alcuni strumenti da riga di comando utilizzano sottocomandi per distribuire diversi task e impacchettarli in «moduli» ognuno dei quali implementato con il pattern Command (come ad esempio Vagrant).

3.2.3. Diagramma UML

Alt Command UML Diagram

3.2.4. Codice

Potete trovare questo codice anche su GitHub

Command.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Command;
 6
 7interface Command
 8{
 9    /**
10     * this is the most important method in the Command pattern,
11     * The Receiver goes in the constructor.
12     */
13    public function execute();
14}

UndoableCommand.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Command;
 6
 7interface UndoableCommand extends Command
 8{
 9    /**
10     * This method is used to undo change made by command execution
11     */
12    public function undo();
13}

HelloCommand.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Command;
 6
 7/**
 8 * This concrete command calls "print" on the Receiver, but an external
 9 * invoker just knows that it can call "execute"
10 */
11class HelloCommand implements Command
12{
13    /**
14     * Each concrete command is built with different receivers.
15     * There can be one, many or completely no receivers, but there can be other commands in the parameters
16     */
17    public function __construct(private Receiver $output)
18    {
19    }
20
21    /**
22     * execute and output "Hello World".
23     */
24    public function execute()
25    {
26        // sometimes, there is no receiver and this is the command which does all the work
27        $this->output->write('Hello World');
28    }
29}

AddMessageDateCommand.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Command;
 6
 7/**
 8 * This concrete command tweaks receiver to add current date to messages
 9 * invoker just knows that it can call "execute"
10 */
11class AddMessageDateCommand implements UndoableCommand
12{
13    /**
14     * Each concrete command is built with different receivers.
15     * There can be one, many or completely no receivers, but there can be other commands in the parameters.
16     */
17    public function __construct(private Receiver $output)
18    {
19    }
20
21    /**
22     * Execute and make receiver to enable displaying messages date.
23     */
24    public function execute()
25    {
26        // sometimes, there is no receiver and this is the command which
27        // does all the work
28        $this->output->enableDate();
29    }
30
31    /**
32     * Undo the command and make receiver to disable displaying messages date.
33     */
34    public function undo()
35    {
36        // sometimes, there is no receiver and this is the command which
37        // does all the work
38        $this->output->disableDate();
39    }
40}

Receiver.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Command;
 6
 7/**
 8 * Receiver is a specific service with its own contract and can be only concrete.
 9 */
10class Receiver
11{
12    private bool $enableDate = false;
13
14    /**
15     * @var string[]
16     */
17    private array $output = [];
18
19    public function write(string $str)
20    {
21        if ($this->enableDate) {
22            $str .= ' [' . date('Y-m-d') . ']';
23        }
24
25        $this->output[] = $str;
26    }
27
28    public function getOutput(): string
29    {
30        return join("\n", $this->output);
31    }
32
33    /**
34     * Enable receiver to display message date
35     */
36    public function enableDate()
37    {
38        $this->enableDate = true;
39    }
40
41    /**
42     * Disable receiver to display message date
43     */
44    public function disableDate()
45    {
46        $this->enableDate = false;
47    }
48}

Invoker.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Command;
 6
 7/**
 8 * Invoker is using the command given to it.
 9 * Example : an Application in SF2.
10 */
11class Invoker
12{
13    private Command $command;
14
15    /**
16     * in the invoker we find this kind of method for subscribing the command
17     * There can be also a stack, a list, a fixed set ...
18     */
19    public function setCommand(Command $cmd)
20    {
21        $this->command = $cmd;
22    }
23
24    /**
25     * executes the command; the invoker is the same whatever is the command
26     */
27    public function run()
28    {
29        $this->command->execute();
30    }
31}

3.2.5. Test

Tests/CommandTest.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Command\Tests;
 6
 7use DesignPatterns\Behavioral\Command\HelloCommand;
 8use DesignPatterns\Behavioral\Command\Invoker;
 9use DesignPatterns\Behavioral\Command\Receiver;
10use PHPUnit\Framework\TestCase;
11
12class CommandTest extends TestCase
13{
14    public function testInvocation()
15    {
16        $invoker = new Invoker();
17        $receiver = new Receiver();
18
19        $invoker->setCommand(new HelloCommand($receiver));
20        $invoker->run();
21        $this->assertSame('Hello World', $receiver->getOutput());
22    }
23}

Tests/UndoableCommandTest.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Command\Tests;
 6
 7use DesignPatterns\Behavioral\Command\AddMessageDateCommand;
 8use DesignPatterns\Behavioral\Command\HelloCommand;
 9use DesignPatterns\Behavioral\Command\Invoker;
10use DesignPatterns\Behavioral\Command\Receiver;
11use PHPUnit\Framework\TestCase;
12
13class UndoableCommandTest extends TestCase
14{
15    public function testInvocation()
16    {
17        $invoker = new Invoker();
18        $receiver = new Receiver();
19
20        $invoker->setCommand(new HelloCommand($receiver));
21        $invoker->run();
22        $this->assertSame('Hello World', $receiver->getOutput());
23
24        $messageDateCommand = new AddMessageDateCommand($receiver);
25        $messageDateCommand->execute();
26
27        $invoker->run();
28        $this->assertSame("Hello World\nHello World [" . date('Y-m-d') . ']', $receiver->getOutput());
29
30        $messageDateCommand->undo();
31
32        $invoker->run();
33        $this->assertSame("Hello World\nHello World [" . date('Y-m-d') . "]\nHello World", $receiver->getOutput());
34    }
35}