3.2. Polecenie (Command)

3.2.1. Przeznaczenie

Pozwala wydzielić wykonanie określonej czynności w postaci obiektu.

Wprowadzamy trzy obiekty: Nadawcę (ang. Invoker), Odbiorcę (ang. Receiver) i Polecenie (ang. Command). Używamy obiektu Polecenia do wykonywania działań na odbiorcy. Dodatkowo wprowadzamy w obiekcie Polecenia metodę execute(). Dzięki temu Nadawca wywołuje zawsze tą samą metodę niezależnie od Polecenia, aby przetworzyć żądanie klienta. Nadawca i Odbiorca nie są ze sobą połączeni.

Drugi aspektem tego wzorca jest metoda undo(), która pozwala cofnąć wywołanie metody execute(). Polecenie może agregować inne Polecenia, aby wykonywać bardziej złożone operacje. Dzięki temu minimalizujemy kopiowanie kodu z innych klas i opieramy się bardziej na kompozycji niż dziedziczeniu.

3.2.2. Przykłady

  • Edytor tekstu: wszystkie zdarzenia (ang. events) są Poleceniami, które można cofnąć undo(), odłożyć na stosie i zapisać.
  • Symfony2: Polecenie w Symfony2, które można uruchomić z poziomu konsoli (CLI - ang. Command Line Interface) zostały oparte na wzorcu projektowym Polecenie.
  • Rozbudowane narzędzia CLI używają Podpoleceń do rozdzielenia różnych zadań i pozwalają na łączenie ich w moduły. Każdy taki moduł może zostać zaimplementowany przy pomocy wzorca Polecenie (np. Vagrant).

3.2.3. Diagram UML

Alt Command UML Diagram

3.2.4. Kod

Ten kod znajdziesz również na GitHub.

CommandInterface.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php

namespace DesignPatterns\Behavioral\Command;

interface CommandInterface
{
    /**
     * this is the most important method in the Command pattern,
     * The Receiver goes in the constructor.
     */
    public function execute();
}

HelloCommand.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

namespace DesignPatterns\Behavioral\Command;

/**
 * This concrete command calls "print" on the Receiver, but an external
 * invoker just knows that it can call "execute"
 */
class HelloCommand implements CommandInterface
{
    /**
     * @var Receiver
     */
    private $output;

    /**
     * Each concrete command is built with different receivers.
     * There can be one, many or completely no receivers, but there can be other commands in the parameters
     *
     * @param Receiver $console
     */
    public function __construct(Receiver $console)
    {
        $this->output = $console;
    }

    /**
     * execute and output "Hello World".
     */
    public function execute()
    {
        // sometimes, there is no receiver and this is the command which does all the work
        $this->output->write('Hello World');
    }
}

Receiver.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

namespace DesignPatterns\Behavioral\Command;

/**
 * Receiver is specific service with its own contract and can be only concrete.
 */
class Receiver
{
    /**
     * @var bool
     */
    private $enableDate = false;

    /**
     * @var string[]
     */
    private $output = [];

    /**
     * @param string $str
     */
    public function write(string $str)
    {
        if ($this->enableDate) {
            $str .= ' ['.date('Y-m-d').']';
        }

        $this->output[] = $str;
    }

    public function getOutput(): string
    {
        return join("\n", $this->output);
    }

    /**
     * Enable receiver to display message date
     */
    public function enableDate()
    {
        $this->enableDate = true;
    }

    /**
     * Disable receiver to display message date
     */
    public function disableDate()
    {
        $this->enableDate = false;
    }
}

Invoker.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

namespace DesignPatterns\Behavioral\Command;

/**
 * Invoker is using the command given to it.
 * Example : an Application in SF2.
 */
class Invoker
{
    /**
     * @var CommandInterface
     */
    private $command;

    /**
     * in the invoker we find this kind of method for subscribing the command
     * There can be also a stack, a list, a fixed set ...
     *
     * @param CommandInterface $cmd
     */
    public function setCommand(CommandInterface $cmd)
    {
        $this->command = $cmd;
    }

    /**
     * executes the command; the invoker is the same whatever is the command
     */
    public function run()
    {
        $this->command->execute();
    }
}

3.2.5. Testy

Tests/CommandTest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace DesignPatterns\Behavioral\Command\Tests;

use DesignPatterns\Behavioral\Command\HelloCommand;
use DesignPatterns\Behavioral\Command\Invoker;
use DesignPatterns\Behavioral\Command\Receiver;
use PHPUnit\Framework\TestCase;

class CommandTest extends TestCase
{
    public function testInvocation()
    {
        $invoker = new Invoker();
        $receiver = new Receiver();

        $invoker->setCommand(new HelloCommand($receiver));
        $invoker->run();
        $this->assertEquals('Hello World', $receiver->getOutput());
    }
}