Шаблоны проектирования по-человечески: поведенческие паттерны в примерах
Поведенческие паттерны отвечают за эффективное взаимодействие объектов. В отличие от структурных, они также затрагивают шаблоны для обмена сообщениями.
Главной проблемой чтения чужого кода могут быть паттерны. Но даже если вы работаете с исконно своим проектом, наша подборка облегчит вашу жизнь. Возьмите на карандаш ;)
Характеристика
Данные шаблоны проектирования определяют способы и алгоритмы реализации совместной работы различных классов и объектов. Поведенческие паттерны уровня класса используют механизм наследования, а шаблоны уровня объекта – механизм композиции. Соседствующие объекты «знают» друг о друге, и важно то, как именно реализована эта связь.
Поведенческие паттерны: классификация
Они делятся на:
- Chain of Responsibility
- Command
- Iterator
- Mediator
- Memento
- Observer
- Visitor
- Strategy
- State
- Template Method
1. Паттерн Chain of Responsibility
Предположим, вы хотите оплатить покупку в Интернете. Всего есть 3 способа оплаты: A, B и C. Каждый из них располагает суммами в $100, $300 и $1000 соответственно, а приоритетность способов снижается от A к C. Вы хотите приобрести товар стоимость $210. Сперва будет проверен баланс A. Если нет достаточной суммы, запрос перемещается к балансу B, и так далее, пока не будет найдена необходимая сумма. Именно так работают поведенческие паттерны Цепочка Обязанностей, где A, B и C – звенья.
Итак, у нас есть базовая учетная запись, в которой объединены дополнительные учетные записи:
abstract class Account { protected $successor; protected $balance; public function setNext(Account $account) { $this->successor = $account; } public function pay(float $amountToPay) { if ($this->canPay($amountToPay)) { echo sprintf('Оплачено %s с использованием %s' . PHP_EOL, $amountToPay, get_called_class()); } elseif ($this->successor) { echo sprintf('Нельзя оплатить с использованием %s. В процессе ..' . PHP_EOL, get_called_class()); $this->successor->pay($amountToPay); } else { throw new Exception('Ни на одном из аккаунтов нет необходимых средств'); } } public function canPay($amount): bool { return $this->balance >= $amount; } } class Bank extends Account { protected $balance; public function __construct(float $balance) { $this->balance = $balance; } } class Paypal extends Account { protected $balance; public function __construct(float $balance) { $this->balance = $balance; } } class Bitcoin extends Account { protected $balance; public function __construct(float $balance) { $this->balance = $balance; } }
Теперь просто подготовим цепь, используя банковский счет, Paypal и Bitcoin:
$bank = new Bank(100); // Банковский счет 100 $paypal = new Paypal(200); // Paypal 200 $bitcoin = new Bitcoin(300); // Bitcoin 300 $bank->setNext($paypal); $paypal->setNext($bitcoin); // Попробуем оплатить с помощью приоритетного банковского счета $bank->pay(259); // Выдаст: // ============== // Нельзя оплатить с использованием bank. В процессе .. // Нельзя оплатить с использованием paypal. В процессе .. // Оплачено 259 с использованием Bitcoin
2. Паттерн Команда
Заказ еды в ресторане: вы (Client) попросите официанта (Invoker) принести еду (Command), а официант отправит этот запрос шеф-повару (Receiver). Другой пример для паттерна Command: вы (Client) включаете (Command) телевизор (Receiver) с помощью пульта (Invoker).
Прежде всего, у нас есть получатель (Receiver), который выполняет определенные действия:
// Receiver class Bulb { public function turnOn() { echo "Лампа светится"; } public function turnOff() { echo "Тьма"; } }
Есть интерфейс реализации команд и набор команд:
interface Command { public function execute(); public function undo(); public function redo(); } // Command class TurnOn implements Command { protected $bulb; public function __construct(Bulb $bulb) { $this->bulb = $bulb; } public function execute() { $this->bulb->turnOn(); } public function undo() { $this->bulb->turnOff(); } public function redo() { $this->execute(); } } class TurnOff implements Command { protected $bulb; public function __construct(Bulb $bulb) { $this->bulb = $bulb; } public function execute() { $this->bulb->turnOff(); } public function undo() { $this->bulb->turnOn(); } public function redo() { $this->execute(); } }
Также должен быть посредник (Invoker), с которым взаимодействует клиент:
// Invoker class RemoteControl { public function submit(Command $command) { $command->execute(); } }
А теперь смотрим, как это все работает:
$bulb = new Bulb(); $turnOn = new TurnOn($bulb); $turnOff = new TurnOff($bulb); $remote = new RemoteControl(); $remote->submit($turnOn); // Лампа светится $remote->submit($turnOff); // Тьма
3. Паттерн Итератор
Хорошим примером станет старый радиоприемник, в котором можно переключаться на следующий и предыдущий каналы с помощью соответствующих кнопок. Нечто подобное можно проделать с телевизором, MP3-плеером и прочими устройствами. Именно поведенческие паттерны Iterator позволяют обходить элементы объекта последовательно.
С PHP это легко реализовать, используя SPL (Standard PHP Library). Разберем пример. Изначально у нас есть радиостанция:
class RadioStation { protected $frequency; public function __construct(float $frequency) { $this->frequency = $frequency; } public function getFrequency(): float { return $this->frequency; } }
Далее вводим итератор:
use Countable; use Iterator; class StationList implements Countable, Iterator { /** @var RadioStation[] $stations */ protected $stations = []; /** @var int $counter */ protected $counter; public function addStation(RadioStation $station) { $this->stations[] = $station; } public function removeStation(RadioStation $toRemove) { $toRemoveFrequency = $toRemove->getFrequency(); $this->stations = array_filter($this->stations, function (RadioStation $station) use ($toRemoveFrequency) { return $station->getFrequency() !== $toRemoveFrequency; }); } public function count(): int { return count($this->stations); } public function current(): RadioStation { return $this->stations[$this->counter]; } public function key() { return $this->counter; } public function next() { $this->counter++; } public function rewind() { $this->counter = 0; } public function valid(): bool { return isset($this->stations[$this->counter]); } }
Реализовываем:
$stationList = new StationList(); $stationList->addStation(new RadioStation(89)); $stationList->addStation(new RadioStation(101)); $stationList->addStation(new RadioStation(102)); $stationList->addStation(new RadioStation(103.2)); foreach($stationList as $station) { echo $station->getFrequency() . PHP_EOL; } $stationList->removeStation(new RadioStation(89)); // Возвращает станцию 89
4. Mediator паттерн
Когда вы разговариваете с кем-то по телефону, это никогда не происходит напрямую. Между вами и собеседником находится провайдер, и в этом случае поставщик мобильных услуг является посредником.
Рассмотрим на примере чата, где и будут использованы поведенческие паттерны Посредник. Есть окно чата:
interface ChatRoomMediator { public function showMessage(User $user, string $message); } // Посредник class ChatRoom implements ChatRoomMediator { public function showMessage(User $user, string $message) { $time = date('M d, y H:i'); $sender = $user->getName(); echo $time . '[' . $sender . ']:' . $message; } }
Добавляем к нему пользователей:
class User { protected $name; protected $chatMediator; public function __construct(string $name, ChatRoomMediator $chatMediator) { $this->name = $name; $this->chatMediator = $chatMediator; } public function getName() { return $this->name; } public function send($message) { $this->chatMediator->showMessage($this, $message); } }
Реализовываем:
$mediator = new ChatRoom(); $john = new User('Джон', $mediator); $jane = new User('Джейн', $mediator); $john->send('Здравствуй!'); $jane->send('Привет!'); // Output will be // Feb 14, 10:58 [Джон]: Здравствуй! // Feb 14, 10:58 [Джейн]: Привет!
5. Memento паттерн
Когда вы делаете какой-либо рассчет с помощью калькулятора, последнее действие сохраняется в памяти устройства. Это нужно для того, чтобы к этому действию можно было вернуться и, возможно, восстановить, нажав на определенные кнопки.
Поведенческие паттерны Memento можно рассмотреть и на примере текстового редактора, который периодически делает сохранения. У нас есть объект, который сможет удерживать состояние редактора:
class EditorMemento { protected $content; public function __construct(string $content) { $this->content = $content; } public function getContent() { return $this->content; } }
Появляется сам редактор:
class Editor { protected $content = ''; public function type(string $words) { $this->content = $this->content . ' ' . $words; } public function getContent() { return $this->content; } public function save() { return new EditorMemento($this->content); } public function restore(EditorMemento $memento) { $this->content = $memento->getContent(); } }
Используем:
$editor = new Editor(); // Ввод $editor->type('Это первое предложение.'); $editor->type('Это второе.'); // Сохраняем состояние для восстановления: Это первое предложение. Это второе. $saved = $editor->save(); // Вводим еще $editor->type('И это уже третье.'); // Вывод: содержание перед сохранением echo $editor->getContent(); // Это первое предложение. Это второе. И это уже третье. // Восстановление последнего сохраненного состояния $editor->restore($saved); $editor->getContent(); // Это первое предложение. Это второе.
6. Паттерн Наблюдатель
Люди, которые ищут работу, часто подписываются на сайты, где публикуются вакансии. Именно эти сайты уведомляют соискателей о подходящих должностях, и именно так работают поведенческие паттерны Observer.
Есть соискатели:
class JobPost { protected $title; public function __construct(string $title) { $this->title = $title; } public function getTitle() { return $this->title; } } class JobSeeker implements Observer { protected $name; public function __construct(string $name) { $this->name = $name; } public function onJobPosted(JobPost $job) { // Do something with the job posting echo 'Привет ' . $this->name . '! Размещена новая вакансия: '. $job->getTitle(); } }
Добавляем вакансии, на которые можно подписываться:
class JobPostings implements Observable { protected $observers = []; protected function notify(JobPost $jobPosting) { foreach ($this->observers as $observer) { $observer->onJobPosted($jobPosting); } } public function attach(Observer $observer) { $this->observers[] = $observer; } public function addJob(JobPost $jobPosting) { $this->notify($jobPosting); } }
Используем:
$johnDoe = new JobSeeker('Джон'); $janeDoe = new JobSeeker('Джейн'); $jobPostings = new JobPostings(); $jobPostings->attach($johnDoe); $jobPostings->attach($janeDoe); $jobPostings->addJob(new JobPost('Разработчик ПО'));
7. Паттерн Visitor
Предположим, вы решили посетить Дубай. Для этого понадобится только виза. По прибытии вы можете посетить любое место города самостоятельно, без необходимости получать дополнительные разрешения. Просто узнайте о нужном месте и посетите его. Поведенческие паттерны Посетитель как раз и отвечают за добавление таких мест, которые можно посещать без дополнительных утруждающих действий.
В качестве примера для кода возьмем зоопарк, где есть разные животные. Зададим интерфейс:
interface Animal { public function accept(AnimalOperation $operation); } interface AnimalOperation { public function visitMonkey(Monkey $monkey); public function visitLion(Lion $lion); public function visitDolphin(Dolphin $dolphin); }
Работаем с разными видами животных:
class Monkey implements Animal { public function shout() { echo 'Ooh oo aa aa!'; } public function accept(AnimalOperation $operation) { $operation->visitMonkey($this); } } class Lion implements Animal { public function roar() { echo 'Roaaar!'; } public function accept(AnimalOperation $operation) { $operation->visitLion($this); } } class Dolphin implements Animal { public function speak() { echo 'Tuut tuttu tuutt!'; } public function accept(AnimalOperation $operation) { $operation->visitDolphin($this); } }
Реализуем посетителя:
class Speak implements AnimalOperation { public function visitMonkey(Monkey $monkey) { $monkey->shout(); } public function visitLion(Lion $lion) { $lion->roar(); } public function visitDolphin(Dolphin $dolphin) { $dolphin->speak(); } }
Используем:
$monkey = new Monkey(); $lion = new Lion(); $dolphin = new Dolphin(); $speak = new Speak(); $monkey->accept($speak); // Ooh oo aa aa! $lion->accept($speak); // Roaaar! $dolphin->accept($speak); // Tuut tutt tuutt!
8. Паттерн Стратегия
Рассмотрим пример сортировки пузырьком. Когда данных становится слишком много, такой вид сортировки становится очень медленным. Чтобы решить проблему, мы применим быструю сортировку. Но хоть этот алгоритм и обрабатывает большие объемы быстро, в небольших он медленный. Поведенческие паттерны Strategy позволяют реализовать стратегию, в которой совмещены оба метода.
Имеем интерфейс и различные варианты реализации стратегии:
interface SortStrategy { public function sort(array $dataset): array; } class BubbleSortStrategy implements SortStrategy { public function sort(array $dataset): array { echo "Сортировка пузырьком"; // Сортируем return $dataset; } } class QuickSortStrategy implements SortStrategy { public function sort(array $dataset): array { echo "Быстрая сортировка"; // Сортируем return $dataset; } }
Теперь есть клиент, выбирающий один из вариантов:
class Sorter { protected $sorter; public function __construct(SortStrategy $sorter) { $this->sorter = $sorter; } public function sort(array $dataset): array { return $this->sorter->sort($dataset); } }
Используем:
$dataset = [1, 5, 4, 3, 2, 8]; $sorter = new Sorter(new BubbleSortStrategy()); $sorter->sort($dataset); // Вывод : Сортировка пузырьком $sorter = new Sorter(new QuickSortStrategy()); $sorter->sort($dataset); // Вывод : Быстрая сортировка
9. Паттерн Состояние
Допустим, вы используете приложение для рисования, где выбираете кисть. Теперь кисть меняет свое состояние в соответствии с выбранным цветом. То есть, если вы выбрали красный цвет, то и кисть будет рисовать красным.
Для кода используем пример с текстовым редактором, в котором можно изменить шрифт. У нас есть интерфейс и реализация некоторых состояний:
interface WritingState { public function write(string $words); } class UpperCase implements WritingState { public function write(string $words) { echo strtoupper($words); } } class LowerCase implements WritingState { public function write(string $words) { echo strtolower($words); } } class Default implements WritingState { public function write(string $words) { echo $words; } }
Добавляем редактор:
class TextEditor { protected $state; public function __construct(WritingState $state) { $this->state = $state; } public function setState(WritingState $state) { $this->state = $state; } public function type(string $words) { $this->state->write($words); } }
Реализовываем:
$editor = new TextEditor(new Default()); $editor->type('Первая строка'); $editor->setState(new UpperCase()); $editor->type('Вторая строка'); $editor->type('Третья строка'); $editor->setState(new LowerCase()); $editor->type('Четвертая строка'); $editor->type('Пятая строка'); // Вывод: // Первая строка // ВТОРАЯ СТРОКА // ТРЕТЬЯ СТРОКА // четвертая строка // пятая строка
10. Паттерн Шаблонный Метод
Строим дом. Этапы работы выглядят так:
- подготовка фундамента;
- строительство стен;
- добавление крыши;
- добавление необходимого количества этажей.
Порядок неизменен, но можно изменить каждый из этапов отдельно. Например, стены могут быть из камня или из дерева.
Предположим, есть инструмент для сборки, который позволяет тестировать программу, анализировать, генерировать отчеты и т. д. Создадим базовый класс-скелет:
abstract class Builder { // Шаблонный Метод final public function build() { $this->test(); $this->lint(); $this->assemble(); $this->deploy(); } abstract public function test(); abstract public function lint(); abstract public function assemble(); abstract public function deploy(); }
Теперь реализации:
class AndroidBuilder extends Builder { public function test() { echo 'Старт Android тестов'; } public function lint() { echo 'Анализ Android кода'; } public function assemble() { echo 'Сборка Android'; } public function deploy() { echo 'Развертывание Android'; } } class IosBuilder extends Builder { public function test() { echo 'Старт iOS тестов'; } public function lint() { echo 'Анализ iOS кода'; } public function assemble() { echo 'Сборка iOS'; } public function deploy() { echo 'Развертывание iOS'; } }
Используем:
$androidBuilder = new AndroidBuilder(); $androidBuilder->build(); $iosBuilder = new IosBuilder(); $iosBuilder->build();