
2024-05-10
Поведенческие шаблоны проектирования (Behavioral)
2023-10-24
Поведенческие паттерны проектирования ( Behavioral ) - определяют общие закономерности связей между объектами, реализующими данные паттерны. Следование этим шаблонам уменьшает связность системы и облегчает коммуникацию между объектами, что улучшает гибкость программного продукта.
Построить цепочку объектов для обработки вызова в последовательном порядке. Если один объект не может справиться с вызовом, он делегирует вызов следующему в цепи и так далее.
Примеры:
Handler.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\ChainOfResponsibilities;
use Psr\Http\Message\RequestInterface;
abstract class Handler
{
public function __construct(private ?Handler $successor = null)
{
}
/**
* This approach by using a template method pattern ensures you that
* each subclass will not forget to call the successor
*/
final public function handle(RequestInterface $request): ?string
{
$processed = $this->processing($request);
if ($processed === null && $this->successor !== null) {
// the request has not been processed by this handler => see the next
$processed = $this->successor->handle($request);
}
return $processed;
}
abstract protected function processing(RequestInterface $request): ?string;
}
Responsible/FastStorage.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use Psr\Http\Message\RequestInterface;
class HttpInMemoryCacheHandler extends Handler
{
public function __construct(private array $data, ?Handler $successor = null)
{
parent::__construct($successor);
}
protected function processing(RequestInterface $request): ?string
{
$key = sprintf(
'%s?%s',
$request->getUri()->getPath(),
$request->getUri()->getQuery()
);
if ($request->getMethod() == 'GET' && isset($this->data[$key])) {
return $this->data[$key];
}
return null;
}
}
Responsible/SlowStorage.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use Psr\Http\Message\RequestInterface;
class SlowDatabaseHandler extends Handler
{
protected function processing(RequestInterface $request): ?string
{
// this is a mockup, in production code you would ask a slow (compared to in-memory) DB for the results
return 'Hello World!';
}
}
Tests/ChainTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Tests;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\HttpInMemoryCacheHandler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowDatabaseHandler;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
class ChainTest extends TestCase
{
private Handler $chain;
protected function setUp(): void
{
$this->chain = new HttpInMemoryCacheHandler(
['/foo/bar?index=1' => 'Hello In Memory!'],
new SlowDatabaseHandler()
);
}
public function testCanRequestKeyInFastStorage()
{
$uri = $this->createMock(UriInterface::class);
$uri->method('getPath')->willReturn('/foo/bar');
$uri->method('getQuery')->willReturn('index=1');
$request = $this->createMock(RequestInterface::class);
$request->method('getMethod')
->willReturn('GET');
$request->method('getUri')->willReturn($uri);
$this->assertSame('Hello In Memory!', $this->chain->handle($request));
}
public function testCanRequestKeyInSlowStorage()
{
$uri = $this->createMock(UriInterface::class);
$uri->method('getPath')->willReturn('/foo/baz');
$uri->method('getQuery')->willReturn('');
$request = $this->createMock(RequestInterface::class);
$request->method('getMethod')
->willReturn('GET');
$request->method('getUri')->willReturn($uri);
$this->assertSame('Hello World!', $this->chain->handle($request));
}
}
Предназначени - инкапсулировать действие и его параметры
Допустим, у нас есть объекты Invoker (Командир) и Receiver (Исполнитель). Этот паттерн использует реализацию интерфейса «Команда», чтобы вызвать некий метод Исполнителя используя для этого известный Командиру метод «execute()». Командир просто знает, что нужно вызвать метод “execute()”, для обработки команды клиента, не разбираясь в деталях реализации Исполнителя. Исполнитель отделен от Командира.
Вторым аспектом этого паттерна является метод undo(), который отменяет действие, выполняемое методом execute(). Команды также могут быть объединены в более общие команды с минимальным копированием-вставкой и полагаясь на композицию поверх наследования.
Примеры:
Command.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Command;
interface Command
{
/**
* this is the most important method in the Command pattern,
* The Receiver goes in the constructor.
*/
public function execute();
}
UnloadableCommand.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Command;
interface UndoableCommand extends Command
{
/**
* This method is used to undo change made by command execution
*/
public function undo();
}
HelloCommand.php
<?php
declare(strict_types=1);
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 Command
{
/**
* 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
*/
public function __construct(private Receiver $output)
{
}
/**
* 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');
}
}
AddMessageDateCommand.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Command;
/**
* This concrete command tweaks receiver to add current date to messages
* invoker just knows that it can call "execute"
*/
class AddMessageDateCommand implements UndoableCommand
{
/**
* 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.
*/
public function __construct(private Receiver $output)
{
}
/**
* Execute and make receiver to enable displaying messages date.
*/
public function execute()
{
// sometimes, there is no receiver and this is the command which
// does all the work
$this->output->enableDate();
}
/**
* Undo the command and make receiver to disable displaying messages date.
*/
public function undo()
{
// sometimes, there is no receiver and this is the command which
// does all the work
$this->output->disableDate();
}
}
Receiver.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Command;
/**
* Receiver is a specific service with its own contract and can be only concrete.
*/
class Receiver
{
private bool $enableDate = false;
/**
* @var string[]
*/
private array $output = [];
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
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Command;
/**
* Invoker is using the command given to it.
* Example : an Application in SF2.
*/
class Invoker
{
private Command $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 ...
*/
public function setCommand(Command $cmd)
{
$this->command = $cmd;
}
/**
* executes the command; the invoker is the same whatever is the command
*/
public function run()
{
$this->command->execute();
}
}
Tests/CommandTest.php
<?php
declare(strict_types=1);
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->assertSame('Hello World', $receiver->getOutput());
}
}
Tests/UnloadableCommandTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Command\Tests;
use DesignPatterns\Behavioral\Command\AddMessageDateCommand;
use DesignPatterns\Behavioral\Command\HelloCommand;
use DesignPatterns\Behavioral\Command\Invoker;
use DesignPatterns\Behavioral\Command\Receiver;
use PHPUnit\Framework\TestCase;
class UndoableCommandTest extends TestCase
{
public function testInvocation()
{
$invoker = new Invoker();
$receiver = new Receiver();
$invoker->setCommand(new HelloCommand($receiver));
$invoker->run();
$this->assertSame('Hello World', $receiver->getOutput());
$messageDateCommand = new AddMessageDateCommand($receiver);
$messageDateCommand->execute();
$invoker->run();
$this->assertSame("Hello World\nHello World [" . date('Y-m-d') . ']', $receiver->getOutput());
$messageDateCommand->undo();
$invoker->run();
$this->assertSame("Hello World\nHello World [" . date('Y-m-d') . "]\nHello World", $receiver->getOutput());
}
}
Для некоего языка шаблон описывает его грамматику с помощью терминов «Терминальный символ» и «Нетерминальный символ», а также описывает интерпретатор предложений, созданных с помощью данного языка.
Например Интерпретатор бинарной (двоичной) логики, в котором каждый тип логической операции определен в своем собственном классе.
AbstractExp.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Interpreter;
abstract class AbstractExp
{
abstract public function interpret(Context $context): bool;
}
Context.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Interpreter;
use Exception;
class Context
{
private array $poolVariable;
public function lookUp(string $name): bool
{
if (!key_exists($name, $this->poolVariable)) {
throw new Exception("no exist variable: $name");
}
return $this->poolVariable[$name];
}
public function assign(VariableExp $variable, bool $val)
{
$this->poolVariable[$variable->getName()] = $val;
}
}
VariableExp.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Interpreter;
/**
* This TerminalExpression
*/
class VariableExp extends AbstractExp
{
public function __construct(private string $name)
{
}
public function interpret(Context $context): bool
{
return $context->lookUp($this->name);
}
public function getName(): string
{
return $this->name;
}
}
AndExp.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Interpreter;
/**
* This NoTerminalExpression
*/
class AndExp extends AbstractExp
{
public function __construct(private AbstractExp $first, private AbstractExp $second)
{
}
public function interpret(Context $context): bool
{
return $this->first->interpret($context) && $this->second->interpret($context);
}
}
OrExp.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Interpreter;
/**
* This NoTerminalExpression
*/
class OrExp extends AbstractExp
{
public function __construct(private AbstractExp $first, private AbstractExp $second)
{
}
public function interpret(Context $context): bool
{
return $this->first->interpret($context) || $this->second->interpret($context);
}
}
Tests/InterpreterTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Interpreter\Tests;
use DesignPatterns\Behavioral\Interpreter\AndExp;
use DesignPatterns\Behavioral\Interpreter\Context;
use DesignPatterns\Behavioral\Interpreter\OrExp;
use DesignPatterns\Behavioral\Interpreter\VariableExp;
use PHPUnit\Framework\TestCase;
class InterpreterTest extends TestCase
{
private Context $context;
private VariableExp $a;
private VariableExp $b;
private VariableExp $c;
public function setUp(): void
{
$this->context = new Context();
$this->a = new VariableExp('A');
$this->b = new VariableExp('B');
$this->c = new VariableExp('C');
}
public function testOr()
{
$this->context->assign($this->a, false);
$this->context->assign($this->b, false);
$this->context->assign($this->c, true);
// A ∨ B
$exp1 = new OrExp($this->a, $this->b);
$result1 = $exp1->interpret($this->context);
$this->assertFalse($result1, 'A ∨ B must false');
// $exp1 ∨ C
$exp2 = new OrExp($exp1, $this->c);
$result2 = $exp2->interpret($this->context);
$this->assertTrue($result2, '(A ∨ B) ∨ C must true');
}
public function testAnd()
{
$this->context->assign($this->a, true);
$this->context->assign($this->b, true);
$this->context->assign($this->c, false);
// A ∧ B
$exp1 = new AndExp($this->a, $this->b);
$result1 = $exp1->interpret($this->context);
$this->assertTrue($result1, 'A ∧ B must true');
// $exp1 ∧ C
$exp2 = new AndExp($exp1, $this->c);
$result2 = $exp2->interpret($this->context);
$this->assertFalse($result2, '(A ∧ B) ∧ C must false');
}
}
Призван добавить коллекции объектов функционал последовательного доступа к содержащимся в ней экземплярам объектов без реализации этого функционала в самой коллекции.
Пример: построчный перебор файла, который представлен в виде объекта, содержащего строки, тоже являющиеся объектами. Обработчик будет запущен поверх всех объектов.
Стандартная библиотека PHP SPL определяет интерфейс Iterator, который хорошо подходит для данных целей. Также вам может понадобиться реализовать интерфейс Countable, чтобы разрешить вызывать count($object) в вашем листаемом объекте.
Book.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Iterator;
class Book
{
public function __construct(private string $title, private string $author)
{
}
public function getAuthor(): string
{
return $this->author;
}
public function getTitle(): string
{
return $this->title;
}
public function getAuthorAndTitle(): string
{
return $this->getTitle() . ' by ' . $this->getAuthor();
}
}
BookList.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Iterator;
use Countable;
use Iterator;
class BookList implements Countable, Iterator
{
/**
* @var Book[]
*/
private array $books = [];
private int $currentIndex = 0;
public function addBook(Book $book)
{
$this->books[] = $book;
}
public function removeBook(Book $bookToRemove)
{
foreach ($this->books as $key => $book) {
if ($book->getAuthorAndTitle() === $bookToRemove->getAuthorAndTitle()) {
unset($this->books[$key]);
}
}
$this->books = array_values($this->books);
}
public function count(): int
{
return count($this->books);
}
public function current(): Book
{
return $this->books[$this->currentIndex];
}
public function key(): int
{
return $this->currentIndex;
}
public function next(): void
{
$this->currentIndex++;
}
public function rewind(): void
{
$this->currentIndex = 0;
}
public function valid(): bool
{
return isset($this->books[$this->currentIndex]);
}
}
Tests/IteratorTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Iterator\Tests;
use DesignPatterns\Behavioral\Iterator\Book;
use DesignPatterns\Behavioral\Iterator\BookList;
use PHPUnit\Framework\TestCase;
class IteratorTest extends TestCase
{
public function testCanIterateOverBookList()
{
$bookList = new BookList();
$bookList->addBook(new Book('Learning PHP Design Patterns', 'William Sanders'));
$bookList->addBook(new Book('Professional Php Design Patterns', 'Aaron Saray'));
$bookList->addBook(new Book('Clean Code', 'Robert C. Martin'));
$books = [];
foreach ($bookList as $book) {
$books[] = $book->getAuthorAndTitle();
}
$this->assertSame(
[
'Learning PHP Design Patterns by William Sanders',
'Professional Php Design Patterns by Aaron Saray',
'Clean Code by Robert C. Martin',
],
$books
);
}
public function testCanIterateOverBookListAfterRemovingBook()
{
$book = new Book('Clean Code', 'Robert C. Martin');
$book2 = new Book('Professional Php Design Patterns', 'Aaron Saray');
$bookList = new BookList();
$bookList->addBook($book);
$bookList->addBook($book2);
$bookList->removeBook($book);
$books = [];
foreach ($bookList as $book) {
$books[] = $book->getAuthorAndTitle();
}
$this->assertSame(
['Professional Php Design Patterns by Aaron Saray'],
$books
);
}
public function testCanAddBookToList()
{
$book = new Book('Clean Code', 'Robert C. Martin');
$bookList = new BookList();
$bookList->addBook($book);
$this->assertCount(1, $bookList);
}
public function testCanRemoveBookFromList()
{
$book = new Book('Clean Code', 'Robert C. Martin');
$bookList = new BookList();
$bookList->addBook($book);
$bookList->removeBook($book);
$this->assertCount(0, $bookList);
}
}
Этот паттерн позволяет снизить связность множества компонентов, работающих совместно. Объектам больше нет нужды вызывать друг друга напрямую. Это хорошая альтернатива Наблюдателю, если у вас есть “центр интеллекта” вроде контроллера (но не в смысле MVC)
Все компоненты (называемые «Коллеги») объединяются в интерфейс Mediator и это хорошо, потому что в рамках ООП, «старый друг лучше новых двух».
Mediator.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Mediator;
interface Mediator
{
public function getUser(string $username): string;
}
Colleague.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Mediator;
abstract class Colleague
{
protected Mediator $mediator;
final public function setMediator(Mediator $mediator)
{
$this->mediator = $mediator;
}
}
Ui.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Mediator;
class Ui extends Colleague
{
public function outputUserInfo(string $username)
{
echo $this->mediator->getUser($username);
}
}
UserRepository.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Mediator;
class UserRepository extends Colleague
{
public function getUserName(string $user): string
{
return 'User: ' . $user;
}
}
UserRepositoryUiMediator.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Mediator;
class UserRepositoryUiMediator implements Mediator
{
public function __construct(private UserRepository $userRepository, private Ui $ui)
{
$this->userRepository->setMediator($this);
$this->ui->setMediator($this);
}
public function printInfoAbout(string $user)
{
$this->ui->outputUserInfo($user);
}
public function getUser(string $username): string
{
return $this->userRepository->getUserName($username);
}
}
Tests/MediatorTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Tests\Mediator\Tests;
use DesignPatterns\Behavioral\Mediator\Ui;
use DesignPatterns\Behavioral\Mediator\UserRepository;
use DesignPatterns\Behavioral\Mediator\UserRepositoryUiMediator;
use PHPUnit\Framework\TestCase;
class MediatorTest extends TestCase
{
public function testOutputHelloWorld()
{
$mediator = new UserRepositoryUiMediator(new UserRepository(), new Ui());
$this->expectOutputString('User: Dominik');
$mediator->printInfoAbout('Dominik');
}
}
Шаблон предоставляет возможность восстановить объект в его предыдущем состоянии (отменить действие посредством отката к предыдущему состоянию) или получить доступ к состоянию объекта, не раскрывая его реализацию (т.е. сам объект не обязан иметь функциональность для возврата текущего состояния).
Шаблон Хранитель реализуется тремя объектами: «Создателем» (originator), «Опекуном» (caretaker) и «Хранитель» (memento).
Хранитель - это объект, который хранит конкретный снимок состояния некоторого объекта или ресурса: строки, числа, массива, экземпляра класса и так далее. Уникальность в данном случае подразумевает не запрет на существование одинаковых состояний в разных снимках, а то, что состояние можно извлечь в виде независимой копии. Любой объект, сохраняемый в Хранителе, должен быть полной копией исходного объекта, а не ссылкой на исходный объект. Сам объект Хранитель является «непрозрачным объектом» (тот, который никто не может и не должен изменять).
Создатель — это объект, который содержит в себе актуальное состояние внешнего объекта строго заданного типа и умеет создавать уникальную копию этого состояния, возвращая её, обёрнутую в объект Хранителя. Создатель не знает истории изменений. Создателю можно принудительно установить конкретное состояние извне, которое будет считаться актуальным. Создатель должен позаботиться о том, чтобы это состояние соответствовало типу объекта, с которым ему разрешено работать. Создатель может (но не обязан) иметь любые методы, но они не могут менять сохранённое состояние объекта.
Опекун управляет историей снимков состояний. Он может вносить изменения в объект, принимать решение о сохранении состояния внешнего объекта в Создателе, запрашивать от Создателя снимок текущего состояния, или привести состояние Создателя в соответствие с состоянием какого-то снимка из истории.
Примеры:
Memento.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Memento;
class Memento
{
public function __construct(private State $state)
{
}
public function getState(): State
{
return $this->state;
}
}
State.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Memento;
use InvalidArgumentException;
class State implements \Stringable
{
public const STATE_CREATED = 'created';
public const STATE_OPENED = 'opened';
public const STATE_ASSIGNED = 'assigned';
public const STATE_CLOSED = 'closed';
private string $state;
/**
* @var string[]
*/
private static array $validStates = [
self::STATE_CREATED,
self::STATE_OPENED,
self::STATE_ASSIGNED,
self::STATE_CLOSED,
];
public function __construct(string $state)
{
self::ensureIsValidState($state);
$this->state = $state;
}
private static function ensureIsValidState(string $state)
{
if (!in_array($state, self::$validStates)) {
throw new InvalidArgumentException('Invalid state given');
}
}
public function __toString(): string
{
return $this->state;
}
}
Ticket.php
?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Memento;
/**
* Ticket is the "Originator" in this implementation
*/
class Ticket
{
private State $currentState;
public function __construct()
{
$this->currentState = new State(State::STATE_CREATED);
}
public function open()
{
$this->currentState = new State(State::STATE_OPENED);
}
public function assign()
{
$this->currentState = new State(State::STATE_ASSIGNED);
}
public function close()
{
$this->currentState = new State(State::STATE_CLOSED);
}
public function saveToMemento(): Memento
{
return new Memento(clone $this->currentState);
}
public function restoreFromMemento(Memento $memento)
{
$this->currentState = $memento->getState();
}
public function getState(): State
{
return $this->currentState;
}
}
Tests/MementoTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Memento\Tests;
use DesignPatterns\Behavioral\Memento\State;
use DesignPatterns\Behavioral\Memento\Ticket;
use PHPUnit\Framework\TestCase;
class MementoTest extends TestCase
{
public function testOpenTicketAssignAndSetBackToOpen()
{
$ticket = new Ticket();
// open the ticket
$ticket->open();
$openedState = $ticket->getState();
$this->assertSame(State::STATE_OPENED, (string) $ticket->getState());
$memento = $ticket->saveToMemento();
// assign the ticket
$ticket->assign();
$this->assertSame(State::STATE_ASSIGNED, (string) $ticket->getState());
// now restore to the opened state, but verify that the state object has been cloned for the memento
$ticket->restoreFromMemento($memento);
$this->assertSame(State::STATE_OPENED, (string) $ticket->getState());
$this->assertNotSame($openedState, $ticket->getState());
}
}
NullObject не шаблон из книги Банды Четырёх, но схема, которая появляется достаточно часто, чтобы считаться паттерном. Она имеет следующие преимущества:
Методы, которые возвращают объект или Null, вместо этого должны вернуть объект NullObject. Это упрощённый формальный код, устраняющий необходимость проверки if (!is_null($obj)) { $obj->callSomething(); }, заменяя её на обычный вызов $obj->callSomething();.
Примеры:
Service.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\NullObject;
class Service
{
public function __construct(private Logger $logger)
{
}
/**
* do something ...
*/
public function doSomething()
{
// notice here that you don't have to check if the logger is set with eg. is_null(), instead just use it
$this->logger->log('We are in ' . __METHOD__);
}
}
Logger.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\NullObject;
/**
* Key feature: NullLogger must inherit from this interface like any other loggers
*/
interface Logger
{
public function log(string $str);
}
PrintLogger.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\NullObject;
class PrintLogger implements Logger
{
public function log(string $str)
{
echo $str;
}
}
NullLogger.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\NullObject;
class NullLogger implements Logger
{
public function log(string $str)
{
// do nothing
}
}
Tests/LoggerTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\NullObject\Tests;
use DesignPatterns\Behavioral\NullObject\NullLogger;
use DesignPatterns\Behavioral\NullObject\PrintLogger;
use DesignPatterns\Behavioral\NullObject\Service;
use PHPUnit\Framework\TestCase;
class LoggerTest extends TestCase
{
public function testNullObject()
{
$service = new Service(new NullLogger());
$this->expectOutputString('');
$service->doSomething();
}
public function testStandardLogger()
{
$service = new Service(new PrintLogger());
$this->expectOutputString('We are in DesignPatterns\Behavioral\NullObject\Service::doSomething');
$service->doSomething();
}
}
Для реализации публикации/подписки на поведение объекта, всякий раз, когда объект «Subject» меняет свое состояние, прикрепленные объекты «Observers» будут уведомлены. Паттерн используется, чтобы сократить количество связанных напрямую объектов и вместо этого использует слабую связь (loose coupling).
Например это может быть, когда система очереди сообщений наблюдает за очередями, чтобы отображать прогресс в GUI
PHP предоставляет два стандартных интерфейса, которые могут помочь реализовать этот шаблон: SplObserver и SplSubject.
User.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Observer;
use SplSubject;
use SplObjectStorage;
use SplObserver;
/**
* User implements the observed object (called Subject), it maintains a list of observers and sends notifications to
* them in case changes are made on the User object
*/
class User implements SplSubject
{
private SplObjectStorage $observers;
private $email;
public function __construct()
{
$this->observers = new SplObjectStorage();
}
public function attach(SplObserver $observer): void
{
$this->observers->attach($observer);
}
public function detach(SplObserver $observer): void
{
$this->observers->detach($observer);
}
public function changeEmail(string $email): void
{
$this->email = $email;
$this->notify();
}
public function notify(): void
{
/** @var SplObserver $observer */
foreach ($this->observers as $observer) {
$observer->update($this);
}
}
}
UserObserver.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Observer;
use SplObserver;
use SplSubject;
class UserObserver implements SplObserver
{
/**
* @var SplSubject[]
*/
private array $changedUsers = [];
/**
* It is called by the Subject, usually by SplSubject::notify()
*/
public function update(SplSubject $subject): void
{
$this->changedUsers[] = clone $subject;
}
/**
* @return SplSubject[]
*/
public function getChangedUsers(): array
{
return $this->changedUsers;
}
}
Tests/ObserverTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Observer\Tests;
use DesignPatterns\Behavioral\Observer\User;
use DesignPatterns\Behavioral\Observer\UserObserver;
use PHPUnit\Framework\TestCase;
class ObserverTest extends TestCase
{
public function testChangeInUserLeadsToUserObserverBeingNotified()
{
$observer = new UserObserver();
$user = new User();
$user->attach($observer);
$user->changeEmail('foo@bar.com');
$this->assertCount(1, $observer->getChangedUsers());
}
}
Строит ясное описание бизнес-правил, на соответствие которым могут быть проверены объекты. Композитный класс спецификация имеет один метод, называемый isSatisfiedBy, который возвращает истину или ложь в зависимости от того, удовлетворяет ли данный объект спецификации.
Item.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Specification;
class Item
{
public function __construct(private float $price)
{
}
public function getPrice(): float
{
return $this->price;
}
}
Specification.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Specification;
interface Specification
{
public function isSatisfiedBy(Item $item): bool;
}
OrSpecification.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Specification;
class OrSpecification implements Specification
{
/**
* @var Specification[]
*/
private array $specifications;
/**
* @param Specification[] $specifications
*/
public function __construct(Specification ...$specifications)
{
$this->specifications = $specifications;
}
/*
* if at least one specification is true, return true, else return false
*/
public function isSatisfiedBy(Item $item): bool
{
foreach ($this->specifications as $specification) {
if ($specification->isSatisfiedBy($item)) {
return true;
}
}
return false;
}
}
PriceSpecification.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Specification;
class PriceSpecification implements Specification
{
public function __construct(private ?float $minPrice, private ?float $maxPrice)
{
}
public function isSatisfiedBy(Item $item): bool
{
if ($this->maxPrice !== null && $item->getPrice() > $this->maxPrice) {
return false;
}
if ($this->minPrice !== null && $item->getPrice() < $this->minPrice) {
return false;
}
return true;
}
}
AndSpecification.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Specification;
class AndSpecification implements Specification
{
/**
* @var Specification[]
*/
private array $specifications;
/**
* @param Specification[] $specifications
*/
public function __construct(Specification ...$specifications)
{
$this->specifications = $specifications;
}
/**
* if at least one specification is false, return false, else return true.
*/
public function isSatisfiedBy(Item $item): bool
{
foreach ($this->specifications as $specification) {
if (!$specification->isSatisfiedBy($item)) {
return false;
}
}
return true;
}
}
NotSpecification.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Specification;
class NotSpecification implements Specification
{
public function __construct(private Specification $specification)
{
}
public function isSatisfiedBy(Item $item): bool
{
return !$this->specification->isSatisfiedBy($item);
}
}
Tests/SpecificationTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Specification\Tests;
use DesignPatterns\Behavioral\Specification\Item;
use DesignPatterns\Behavioral\Specification\NotSpecification;
use DesignPatterns\Behavioral\Specification\OrSpecification;
use DesignPatterns\Behavioral\Specification\AndSpecification;
use DesignPatterns\Behavioral\Specification\PriceSpecification;
use PHPUnit\Framework\TestCase;
class SpecificationTest extends TestCase
{
public function testCanOr()
{
$spec1 = new PriceSpecification(50, 99);
$spec2 = new PriceSpecification(101, 200);
$orSpec = new OrSpecification($spec1, $spec2);
$this->assertFalse($orSpec->isSatisfiedBy(new Item(100)));
$this->assertTrue($orSpec->isSatisfiedBy(new Item(51)));
$this->assertTrue($orSpec->isSatisfiedBy(new Item(150)));
}
public function testCanAnd()
{
$spec1 = new PriceSpecification(50, 100);
$spec2 = new PriceSpecification(80, 200);
$andSpec = new AndSpecification($spec1, $spec2);
$this->assertFalse($andSpec->isSatisfiedBy(new Item(150)));
$this->assertFalse($andSpec->isSatisfiedBy(new Item(1)));
$this->assertFalse($andSpec->isSatisfiedBy(new Item(51)));
$this->assertTrue($andSpec->isSatisfiedBy(new Item(100)));
}
public function testCanNot()
{
$spec1 = new PriceSpecification(50, 100);
$notSpec = new NotSpecification($spec1);
$this->assertTrue($notSpec->isSatisfiedBy(new Item(150)));
$this->assertFalse($notSpec->isSatisfiedBy(new Item(50)));
}
}
Инкапсулирует изменение поведения одних и тех же методов в зависимости от состояния объекта. Этот паттерн поможет изящным способом изменить поведение объекта во время выполнения не прибегая к большим монолитным условным операторам.
OrderContext.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\State;
class OrderContext
{
private State $state;
public static function create(): OrderContext
{
$order = new self();
$order->state = new StateCreated();
return $order;
}
public function setState(State $state)
{
$this->state = $state;
}
public function proceedToNext()
{
$this->state->proceedToNext($this);
}
public function toString()
{
return $this->state->toString();
}
}
State.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\State;
interface State
{
public function proceedToNext(OrderContext $context);
public function toString(): string;
}
StateCreated.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\State;
class StateCreated implements State
{
public function proceedToNext(OrderContext $context)
{
$context->setState(new StateShipped());
}
public function toString(): string
{
return 'created';
}
}
StateShipped.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\State;
class StateShipped implements State
{
public function proceedToNext(OrderContext $context)
{
$context->setState(new StateDone());
}
public function toString(): string
{
return 'shipped';
}
}
StateDone.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\State;
class StateDone implements State
{
public function proceedToNext(OrderContext $context)
{
// there is nothing more to do
}
public function toString(): string
{
return 'done';
}
}
Tests/StateTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\State\Tests;
use DesignPatterns\Behavioral\State\OrderContext;
use PHPUnit\Framework\TestCase;
class StateTest extends TestCase
{
public function testIsCreatedWithStateCreated()
{
$orderContext = OrderContext::create();
$this->assertSame('created', $orderContext->toString());
}
public function testCanProceedToStateShipped()
{
$contextOrder = OrderContext::create();
$contextOrder->proceedToNext();
$this->assertSame('shipped', $contextOrder->toString());
}
public function testCanProceedToStateDone()
{
$contextOrder = OrderContext::create();
$contextOrder->proceedToNext();
$contextOrder->proceedToNext();
$this->assertSame('done', $contextOrder->toString());
}
public function testStateDoneIsTheLastPossibleState()
{
$contextOrder = OrderContext::create();
$contextOrder->proceedToNext();
$contextOrder->proceedToNext();
$contextOrder->proceedToNext();
$this->assertSame('done', $contextOrder->toString());
}
}
Чтобы разделить стратегии и получить возможность быстрого переключения между ними. Также этот паттерн является хорошей альтернативой наследованию (вместо расширения абстрактного класса).
Примеры:
Context.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Strategy;
class Context
{
public function __construct(private Comparator $comparator)
{
}
public function executeStrategy(array $elements): array
{
uasort($elements, [$this->comparator, 'compare']);
return $elements;
}
}
Comparator.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Strategy;
interface Comparator
{
/**
* @param mixed $a
* @param mixed $b
*/
public function compare($a, $b): int;
}
DateComparator.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Strategy;
use DateTime;
class DateComparator implements Comparator
{
public function compare($a, $b): int
{
$aDate = new DateTime($a['date']);
$bDate = new DateTime($b['date']);
return $aDate <=> $bDate;
}
}
IdComparator.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Strategy;
class IdComparator implements Comparator
{
public function compare($a, $b): int
{
return $a['id'] <=> $b['id'];
}
}
Tests/StrategyTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Strategy\Tests;
use DesignPatterns\Behavioral\Strategy\Context;
use DesignPatterns\Behavioral\Strategy\DateComparator;
use DesignPatterns\Behavioral\Strategy\IdComparator;
use PHPUnit\Framework\TestCase;
class StrategyTest extends TestCase
{
public function provideIntegers()
{
return [
[
[['id' => 2], ['id' => 1], ['id' => 3]],
['id' => 1],
],
[
[['id' => 3], ['id' => 2], ['id' => 1]],
['id' => 1],
],
];
}
public function provideDates()
{
return [
[
[['date' => '2014-03-03'], ['date' => '2015-03-02'], ['date' => '2013-03-01']],
['date' => '2013-03-01'],
],
[
[['date' => '2014-02-03'], ['date' => '2013-02-01'], ['date' => '2015-02-02']],
['date' => '2013-02-01'],
],
];
}
/**
* @dataProvider provideIntegers
*
* @param array $collection
* @param array $expected
*/
public function testIdComparator($collection, $expected)
{
$obj = new Context(new IdComparator());
$elements = $obj->executeStrategy($collection);
$firstElement = array_shift($elements);
$this->assertSame($expected, $firstElement);
}
/**
* @dataProvider provideDates
*
* @param array $collection
* @param array $expected
*/
public function testDateComparator($collection, $expected)
{
$obj = new Context(new DateComparator());
$elements = $obj->executeStrategy($collection);
$firstElement = array_shift($elements);
$this->assertSame($expected, $firstElement);
}
}
Шаблонный метод, это поведенческий паттерн проектирования.
Возможно, вы сталкивались с этим уже много раз. Идея состоит в том, чтобы позволить наследникам абстрактного шаблона переопределить поведение алгоритмов родителя.
Как в «Голливудском принципе»: «Не звоните нам, мы сами вам позвоним». Этот класс не вызывается подклассами, но наоборот: подклассы вызываются родителем. Как? С помощью метода в родительской абстракции, конечно.
Другими словами, это каркас алгоритма, который хорошо подходит для библиотек (в фреймворках, например). Пользователь просто реализует уточняющие методы, а суперкласс делает всю основную работу.
Это простой способ изолировать логику в конкретные классы и уменьшить копипаст, поэтому вы повсеместно встретите его в том или ином виде.
Journey.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\TemplateMethod;
abstract class Journey
{
/**
* @var string[]
*/
private array $thingsToDo = [];
/**
* This is the public service provided by this class and its subclasses.
* Notice it is final to "freeze" the global behavior of algorithm.
* If you want to override this contract, make an interface with only takeATrip()
* and subclass it.
*/
final public function takeATrip()
{
$this->thingsToDo[] = $this->buyAFlight();
$this->thingsToDo[] = $this->takePlane();
$this->thingsToDo[] = $this->enjoyVacation();
$buyGift = $this->buyGift();
if ($buyGift !== null) {
$this->thingsToDo[] = $buyGift;
}
$this->thingsToDo[] = $this->takePlane();
}
/**
* This method must be implemented, this is the key-feature of this pattern.
*/
abstract protected function enjoyVacation(): string;
/**
* This method is also part of the algorithm but it is optional.
* You can override it only if you need to
*/
protected function buyGift(): ?string
{
return null;
}
private function buyAFlight(): string
{
return 'Buy a flight ticket';
}
private function takePlane(): string
{
return 'Taking the plane';
}
/**
* @return string[]
*/
final public function getThingsToDo(): array
{
return $this->thingsToDo;
}
}
BeachJourney.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\TemplateMethod;
class BeachJourney extends Journey
{
protected function enjoyVacation(): string
{
return "Swimming and sun-bathing";
}
}
CityJourney.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\TemplateMethod;
class CityJourney extends Journey
{
protected function enjoyVacation(): string
{
return "Eat, drink, take photos and sleep";
}
protected function buyGift(): ?string
{
return "Buy a gift";
}
}
Tests/JourneyTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\TemplateMethod\Tests;
use DesignPatterns\Behavioral\TemplateMethod\BeachJourney;
use DesignPatterns\Behavioral\TemplateMethod\CityJourney;
use PHPUnit\Framework\TestCase;
class JourneyTest extends TestCase
{
public function testCanGetOnVacationOnTheBeach()
{
$beachJourney = new BeachJourney();
$beachJourney->takeATrip();
$this->assertSame(
['Buy a flight ticket', 'Taking the plane', 'Swimming and sun-bathing', 'Taking the plane'],
$beachJourney->getThingsToDo()
);
}
public function testCanGetOnAJourneyToACity()
{
$cityJourney = new CityJourney();
$cityJourney->takeATrip();
$this->assertSame(
[
'Buy a flight ticket',
'Taking the plane',
'Eat, drink, take photos and sleep',
'Buy a gift',
'Taking the plane'
],
$cityJourney->getThingsToDo()
);
}
}
Шаблон «Посетитель» выполняет операции над объектами других классов. Главной целью является сохранение разделения направленности задач отдельных классов. При этом классы обязаны определить специальный контракт, чтобы позволить использовать их Посетителям (метод «принять роль» Role::accept в примере).
Контракт, как правило, это абстрактный класс, но вы можете использовать чистый интерфейс. В этом случае, каждый посетитель должен сам выбирать, какой метод ссылается на посетителя.
RoleVisitor.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Visitor;
/**
* Note: the visitor must not choose itself which method to
* invoke, it is the visited object that makes this decision
*/
interface RoleVisitor
{
public function visitUser(User $role);
public function visitGroup(Group $role);
}
RecordingVisitor.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Visitor;
class RecordingVisitor implements RoleVisitor
{
/**
* @var Role[]
*/
private array $visited = [];
public function visitGroup(Group $role)
{
$this->visited[] = $role;
}
public function visitUser(User $role)
{
$this->visited[] = $role;
}
/**
* @return Role[]
*/
public function getVisited(): array
{
return $this->visited;
}
}
Role.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Visitor;
interface Role
{
public function accept(RoleVisitor $visitor);
}
User.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Visitor;
class User implements Role
{
public function __construct(private string $name)
{
}
public function getName(): string
{
return sprintf('User %s', $this->name);
}
public function accept(RoleVisitor $visitor)
{
$visitor->visitUser($this);
}
}
Group.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Behavioral\Visitor;
class Group implements Role
{
public function __construct(private string $name)
{
}
public function getName(): string
{
return sprintf('Group: %s', $this->name);
}
public function accept(RoleVisitor $visitor)
{
$visitor->visitGroup($this);
}
}
Tests/VisitorTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Tests\Visitor\Tests;
use DesignPatterns\Behavioral\Visitor\RecordingVisitor;
use DesignPatterns\Behavioral\Visitor\User;
use DesignPatterns\Behavioral\Visitor\Group;
use DesignPatterns\Behavioral\Visitor\Role;
use DesignPatterns\Behavioral\Visitor;
use PHPUnit\Framework\TestCase;
class VisitorTest extends TestCase
{
private RecordingVisitor $visitor;
protected function setUp(): void
{
$this->visitor = new RecordingVisitor();
}
public function provideRoles()
{
return [
[new User('Dominik')],
[new Group('Administrators')],
];
}
/**
* @dataProvider provideRoles
*/
public function testVisitSomeRole(Role $role)
{
$role->accept($this->visitor);
$this->assertSame($role, $this->visitor->getVisited()[0]);
}
}
Порождающие шаблоны проектирования (Creational)
Ура! Я наконец-то дописал статью как собирать собственные бандлы на Symfony 6!!!
Статья про EasyAdmin всё ещё в процессе )))
Не, ну мне же надо на чем-то тестировать твиттер локальный...
Я тут еще много полезного буду выкладывать, так что заходите обязательно почитать.
Сайтик пока что в разработке - это далеко не окончательная версия - по сути это то что удалось слепить за 8 часов.
Комментарии