Перейти к основному содержимому

6. Паттерны проектирования

Паттерны проектирования тесно связаны с разделом Чистый код и лучшие практики.

Какие основные категории паттернов проектирования существуют?​

Junior

Ответ: Паттерны проектирования обычно делятся на три группы:

  • Порождающие паттерны (Creational Patterns): Отвечают за механизмы создания объектов.
  • Структурные паттерны (Structural Patterns): Отвечают за то, как классы и объекты компонуются в более крупные структуры.
  • Поведенческие паттерны (Behavioral Patterns): Отвечают за взаимодействие между объектами.

Что такое паттерн Singleton?​

Junior

Ответ: Паттерн, который гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. Относится к порождающим паттернам.


Что такое паттерн Factory Method?​

Middle

Ответ: Factory Method — это порождающий паттерн проектирования, который определяет интерфейс для создания объектов в суперклассе, но позволяет подклассам изменять тип создаваемых объектов.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы избегаете жёсткой связи между создателем и конкретными продуктами. Принцип единственной ответственности (SRP). Вы можете вынести код создания продуктов в одно место программы, что упрощает поддержку кода. Принцип открытости/закрытости (OCP). Вы можете вводить новые типы продуктов в программу, не нарушая существующий клиентский код.
  • Минусы: Код может стать сложнее, поскольку для реализации паттерна необходимо ввести множество новых подклассов. Лучший вариант — когда вы внедряете паттерн в уже существующую иерархию классов-создателей.

Пример:

<?php

// Класс-создатель объявляет фабричный метод, который должен
// возвращать объект класса продукта. Подклассы создателя обычно
// предоставляют реализацию этого метода.
abstract class Dialog
{
// Создатель также может предоставить реализацию фабричного
// метода по умолчанию.
abstract protected function createButton(): Button;

// Обратите внимание: несмотря на название, основная
// ответственность создателя — не создание продуктов.
// Обычно он содержит основную бизнес-логику, которая
// опирается на объекты продуктов, возвращаемые фабричным
// методом. Подклассы могут косвенно изменять эту
// бизнес-логику, переопределяя фабричный метод и возвращая
// из него другой тип продукта.
public function render(): void
{
// Вызываем фабричный метод для создания объекта продукта.
$okButton = $this->createButton();
// Используем продукт.
$okButton->onClick([$this, 'closeDialog']);
$okButton->render();
}

protected function closeDialog(): void
{
echo "Closing dialog...\n";
}
}

// Конкретные создатели переопределяют фабричный метод, чтобы
// изменить тип возвращаемого продукта.
class WindowsDialog extends Dialog
{
protected function createButton(): Button
{
return new WindowsButton();
}
}

class WebDialog extends Dialog
{
protected function createButton(): Button
{
return new HTMLButton();
}
}

// Интерфейс продукта объявляет операции, которые должны
// реализовать все конкретные продукты.
interface Button
{
public function render(): void;
public function onClick(callable $callback): void;
}

// Конкретные продукты предоставляют различные реализации
// интерфейса продукта.
class WindowsButton implements Button
{
public function render(): void
{
echo "Render a button in Windows style.\n";
}

public function onClick(callable $callback): void
{
echo "Bind a native OS click event.\n";
$callback();
}
}

class HTMLButton implements Button
{
public function render(): void
{
echo "Return an HTML representation of a button.\n";
}

public function onClick(callable $callback): void
{
echo "Bind a web browser click event.\n";
$callback();
}
}

class Application
{
private Dialog $dialog;

// Приложение выбирает тип создателя в зависимости от
// текущей конфигурации или настроек среды.
public function initialize(string $os): void
{
if ($os === 'Windows') {
$this->dialog = new WindowsDialog();
} elseif ($os === 'Web') {
$this->dialog = new WebDialog();
} else {
throw new InvalidArgumentException('Error! Unknown operating system.');
}
}

// Клиентский код работает с экземпляром конкретного
// создателя, но через его базовый интерфейс. Пока клиент
// продолжает работать с создателем через базовый интерфейс,
// ему можно передать любой подкласс создателя.
public function main(string $os): void
{
$this->initialize($os);
$this->dialog->render();
}
}

$app = new Application();
$app->main('Windows');

Что такое паттерн Abstract Factory?​

Middle

Ответ: Abstract Factory — это порождающий паттерн проектирования, который позволяет создавать семейства связанных объектов без указания их конкретных классов.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете быть уверены, что продукты, получаемые от фабрики, совместимы друг с другом. Вы избегаете жёсткой связи между конкретными продуктами и клиентским кодом. Принцип единственной ответственности (SRP). Вы можете вынести код создания продуктов в одно место, что упрощает поддержку кода. Принцип открытости/закрытости (OCP). Вы можете вводить новые варианты продуктов, не нарушая существующий клиентский код.
  • Минусы: Код может стать сложнее, чем нужно, поскольку вместе с паттерном вводится множество новых интерфейсов и классов.

Примечание: В чём разница между Abstract Factory и Factory?

Ответ:

  • Factory Method определяет интерфейс для создания одного продукта, но подклассы решают, какой класс инстанцировать.
  • Abstract Factory предоставляет интерфейс для создания семейств связанных или зависимых объектов.
  • Factory Method основан на наследовании (реализуется в подклассах), тогда как Abstract Factory основан на композиции объектов (фабрика — это объект, передаваемый клиенту).

Пример:

<?php

// Интерфейс абстрактной фабрики объявляет набор методов,
// возвращающих различные абстрактные продукты. Эти продукты
// называются семейством и связаны общей темой или концепцией
// высокого уровня. Продукты одного семейства обычно способны
// взаимодействовать друг с другом. Семейство продуктов может
// иметь несколько вариантов, но продукты одного варианта
// несовместимы с продуктами другого варианта.
interface GUIFactory
{
public function createButton(): Button;
public function createCheckbox(): Checkbox;
}

// Конкретные фабрики производят семейство продуктов,
// принадлежащих одному варианту. Фабрика гарантирует
// совместимость полученных продуктов. Сигнатуры методов
// конкретной фабрики возвращают абстрактный продукт, тогда как
// внутри метода создаётся конкретный продукт.
class WinFactory implements GUIFactory
{
public function createButton(): Button
{
return new WinButton();
}

public function createCheckbox(): Checkbox
{
return new WinCheckbox();
}
}

// Каждая конкретная фабрика имеет соответствующий вариант
// продуктов.
class MacFactory implements GUIFactory
{
public function createButton(): Button
{
return new MacButton();
}

public function createCheckbox(): Checkbox
{
return new MacCheckbox();
}
}

// Каждый отдельный продукт семейства должен иметь базовый
// интерфейс. Все варианты продукта должны реализовывать
// этот интерфейс.
interface Button
{
public function paint(): void;
}

// Конкретные продукты создаются соответствующими конкретными
// фабриками.
class WinButton implements Button
{
public function paint(): void
{
echo "Render a button in Windows style.\n";
}
}

class MacButton implements Button
{
public function paint(): void
{
echo "Render a button in macOS style.\n";
}
}

// Базовый интерфейс другого продукта. Все продукты могут
// взаимодействовать друг с другом, но корректное взаимодействие
// возможно только между продуктами одного конкретного варианта.
interface Checkbox
{
public function paint(): void;
}

class WinCheckbox implements Checkbox
{
public function paint(): void
{
echo "Render a checkbox in Windows style.\n";
}
}

class MacCheckbox implements Checkbox
{
public function paint(): void
{
echo "Render a checkbox in macOS style.\n";
}
}

// Клиентский код работает с фабриками и продуктами только
// через абстрактные типы: GUIFactory, Button и Checkbox. Это
// позволяет передавать клиентскому коду любой подкласс фабрики
// или продукта, не нарушая его работу.
class Application
{
private GUIFactory $factory;
private Button $button;
private Checkbox $checkbox;

public function __construct(GUIFactory $factory)
{
$this->factory = $factory;
}

public function createUI(): void
{
$this->button = $this->factory->createButton();
$this->checkbox = $this->factory->createCheckbox();
}

public function paint(): void
{
$this->button->paint();
$this->checkbox->paint();
}
}

// Приложение выбирает тип фабрики в зависимости от текущей
// конфигурации или настроек среды и создаёт её во время
// выполнения (обычно на этапе инициализации).
class ApplicationConfigurator
{
public function main(string $os): void
{
if ($os === 'Windows') {
$factory = new WinFactory();
} elseif ($os === 'Mac') {
$factory = new MacFactory();
} else {
throw new InvalidArgumentException('Error! Unknown operating system.');
}

$app = new Application($factory);
$app->createUI();
$app->paint();
}
}

$configurator = new ApplicationConfigurator();
$configurator->main('Mac');

Что такое паттерн Builder?​

Middle

Ответ: Builder — это порождающий паттерн проектирования, который позволяет создавать сложные объекты пошагово. Паттерн даёт возможность производить различные типы и представления объекта, используя один и тот же код конструирования.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете конструировать объекты пошагово, откладывать шаги конструирования или выполнять их рекурсивно. Вы можете повторно использовать один и тот же код конструирования для построения различных представлений продуктов. Принцип единственной ответственности (SRP). Вы можете изолировать сложный код конструирования от бизнес-логики продукта.
  • Минусы: Общая сложность кода возрастает, поскольку паттерн требует создания нескольких новых классов.

Пример:

<?php

// Использование паттерна Builder имеет смысл только тогда,
// когда ваши продукты достаточно сложны и требуют обширной
// настройки. Следующие два продукта связаны между собой, хотя
// и не имеют общего интерфейса.
class Car
{
public int $seats = 0;
public string $engine = '';
public bool $tripComputer = false;
public bool $gps = false;
}

class Manual
{
/** @var string[] */
public array $instructions = [];
}

class SportEngine
{
public function __toString(): string
{
return 'SportEngine';
}
}

// Интерфейс строителя определяет методы для создания различных
// частей объектов-продуктов.
interface Builder
{
public function reset(): void;
public function setSeats(int $count): void;
public function setEngine(string $engine): void;
public function setTripComputer(bool $enabled): void;
public function setGPS(bool $enabled): void;
}

// Конкретные классы строителей следуют интерфейсу строителя и
// предоставляют специфические реализации шагов построения. Ваша
// программа может иметь несколько вариантов строителей, каждый
// реализованный по-своему.
class CarBuilder implements Builder
{
private Car $car;

// Свежий экземпляр строителя должен содержать пустой объект
// продукта, который он использует при дальнейшей сборке.
public function __construct()
{
$this->reset();
}

// Метод reset очищает строящийся объект.
public function reset(): void
{
$this->car = new Car();
}

// Все шаги производства работают с одним и тем же
// экземпляром продукта.
public function setSeats(int $count): void
{
$this->car->seats = $count;
}

public function setEngine(string $engine): void
{
$this->car->engine = $engine;
}

public function setTripComputer(bool $enabled): void
{
$this->car->tripComputer = $enabled;
}

public function setGPS(bool $enabled): void
{
$this->car->gps = $enabled;
}

// Конкретные строители должны предоставлять собственные
// методы для получения результатов. Это связано с тем, что
// различные типы строителей могут создавать совершенно
// разные продукты, не имеющие общего интерфейса. Поэтому
// такие методы нельзя объявить в интерфейсе строителя (по
// крайней мере, в статически типизированном языке
// программирования).
//
// Обычно после возврата конечного результата клиенту
// экземпляр строителя должен быть готов к созданию другого
// продукта. Поэтому принято вызывать метод reset в конце
// тела метода `getProduct`. Однако это поведение не является
// обязательным, и вы можете сделать так, чтобы строитель
// ждал явного вызова reset от клиентского кода перед
// удалением предыдущего результата.
public function getProduct(): Car
{
$product = $this->car;
$this->reset();

return $product;
}
}

// В отличие от других порождающих паттернов, Builder позволяет
// создавать продукты, не имеющие общего интерфейса.
class CarManualBuilder implements Builder
{
private Manual $manual;

public function __construct()
{
$this->reset();
}

public function reset(): void
{
$this->manual = new Manual();
}

public function setSeats(int $count): void
{
$this->manual->instructions[] = "Seats: {$count}";
}

public function setEngine(string $engine): void
{
$this->manual->instructions[] = "Engine: {$engine}";
}

public function setTripComputer(bool $enabled): void
{
$this->manual->instructions[] = 'Trip computer: ' . ($enabled ? 'enabled' : 'disabled');
}

public function setGPS(bool $enabled): void
{
$this->manual->instructions[] = 'GPS: ' . ($enabled ? 'enabled' : 'disabled');
}

public function getProduct(): Manual
{
$product = $this->manual;
$this->reset();

return $product;
}
}

// Директор отвечает только за выполнение шагов построения в
// определённой последовательности. Он полезен при производстве
// продуктов по конкретному порядку или конфигурации. Строго
// говоря, класс директора необязателен, поскольку клиент может
// управлять строителями напрямую.
class Director
{
// Директор работает с любым экземпляром строителя, который
// передаёт ему клиентский код. Таким образом, клиентский
// код может изменить конечный тип вновь собираемого продукта.
// Директор может конструировать несколько вариантов
// продуктов, используя одни и те же шаги построения.
public function constructSportsCar(Builder $builder): void
{
$builder->reset();
$builder->setSeats(2);
$builder->setEngine((string) new SportEngine());
$builder->setTripComputer(true);
$builder->setGPS(true);
}

public function constructSUV(Builder $builder): void
{
$builder->reset();
$builder->setSeats(5);
$builder->setEngine('DieselEngine');
$builder->setTripComputer(true);
$builder->setGPS(true);
}
}

// Клиентский код создаёт объект строителя, передаёт его
// директору и затем инициирует процесс конструирования.
// Конечный результат извлекается из объекта строителя.
class Application
{
public function makeCar(): void
{
$director = new Director();

$carBuilder = new CarBuilder();
$director->constructSportsCar($carBuilder);
$car = $carBuilder->getProduct();

$manualBuilder = new CarManualBuilder();
$director->constructSportsCar($manualBuilder);

// Конечный продукт часто извлекается из объекта
// строителя, поскольку директор не знает о конкретных
// строителях и продуктах и не зависит от них.
$manual = $manualBuilder->getProduct();

var_dump($car, $manual->instructions);
}
}

$app = new Application();
$app->makeCar();

Что такое паттерн Prototype?​

Middle

Ответ: Prototype — это порождающий паттерн проектирования, который позволяет копировать существующие объекты, не делая код зависимым от их классов.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете клонировать объекты без привязки к их конкретным классам. Вы можете избавиться от повторяющегося кода инициализации в пользу клонирования заранее созданных прототипов. Вы можете удобнее создавать сложные объекты. Вы получаете альтернативу наследованию при работе с конфигурационными пресетами сложных объектов.
  • Минусы: Клонирование сложных объектов с циклическими ссылками может оказаться весьма непростой задачей.

Пример:

<?php

// Базовый прототип.
abstract class Shape
{
public int $x = 0;
public int $y = 0;
public string $color = 'black';

// Конструктор прототипа. Новый объект инициализируется
// значениями из существующего объекта.
public function __construct(?Shape $source = null)
{
if ($source !== null) {
$this->x = $source->x;
$this->y = $source->y;
$this->color = $source->color;
}
}

// Операция клонирования возвращает один из подклассов Shape.
abstract public function copy(): Shape;
}

// Конкретный прототип. Метод клонирования создаёт новый объект
// одним вызовом конструктора текущего класса, передавая текущий
// объект в качестве аргумента конструктора. Выполнение всего
// фактического копирования в конструкторе помогает сохранить
// согласованность результата: конструктор не вернёт результат,
// пока новый объект не будет полностью построен; таким образом,
// ни один объект не может иметь ссылку на частично
// построенный клон.
class Rectangle extends Shape
{
public int $width = 0;
public int $height = 0;

public function __construct(?Rectangle $source = null)
{
parent::__construct($source);

if ($source !== null) {
$this->width = $source->width;
$this->height = $source->height;
}
}

public function copy(): Shape
{
return new Rectangle($this);
}
}

class Circle extends Shape
{
public int $radius = 0;

public function __construct(?Circle $source = null)
{
parent::__construct($source);

if ($source !== null) {
$this->radius = $source->radius;
}
}

public function copy(): Shape
{
return new Circle($this);
}
}

// Где-то в клиентском коде.
class Application
{
/** @var Shape[] */
private array $shapes = [];

public function __construct()
{
$circle = new Circle();
$circle->x = 10;
$circle->y = 10;
$circle->radius = 20;
$this->shapes[] = $circle;

$anotherCircle = $circle->copy();
$this->shapes[] = $anotherCircle;
// Переменная `anotherCircle` содержит точную копию
// объекта `circle`.

$rectangle = new Rectangle();
$rectangle->width = 10;
$rectangle->height = 20;
$this->shapes[] = $rectangle;
}

public function businessLogic(): void
{
// Prototype хорош тем, что позволяет создавать копию
// объекта, ничего не зная о его типе.
$shapesCopy = [];

// Например, мы не знаем точных элементов в массиве
// shapes. Мы знаем лишь то, что все они являются
// фигурами. Но благодаря полиморфизму, когда мы
// вызываем метод `copy` у фигуры, программа проверяет
// её реальный класс и запускает соответствующий метод
// копирования, определённый в этом классе. Поэтому мы
// получаем правильные клоны, а не набор простых объектов
// Shape.
foreach ($this->shapes as $shape) {
$shapesCopy[] = $shape->copy();
}

// Массив `shapesCopy` содержит точные копии дочерних
// элементов массива `shapes`.
var_dump($shapesCopy);
}
}

$app = new Application();
$app->businessLogic();

Что такое паттерн Adapter?​

Middle

Ответ: Adapter — это структурный паттерн проектирования, который позволяет объектам с несовместимыми интерфейсами работать вместе.

Типичное использование / Плюсы и минусы:

  • Плюсы: Принцип единственной ответственности (SRP). Вы можете отделить код преобразования интерфейса или данных от основной бизнес-логики программы. Принцип открытости/закрытости (OCP). Вы можете вводить новые типы адаптеров в программу, не нарушая существующий клиентский код, при условии, что они работают с адаптерами через клиентский интерфейс.
  • Минусы: Общая сложность кода возрастает, поскольку необходимо ввести набор новых интерфейсов и классов. Иногда проще изменить класс сервиса так, чтобы он соответствовал остальному коду.

Пример:

<?php

// Допустим, у вас есть два класса с совместимыми интерфейсами:
// RoundHole и RoundPeg.
class RoundHole
{
public function __construct(private float $radius)
{
}

public function getRadius(): float
{
return $this->radius;
}

public function fits(RoundPeg $peg): bool
{
return $this->getRadius() >= $peg->getRadius();
}
}

class RoundPeg
{
public function __construct(private float $radius)
{
}

public function getRadius(): float
{
return $this->radius;
}
}

// Но есть несовместимый класс: SquarePeg.
class SquarePeg
{
public function __construct(private float $width)
{
}

public function getWidth(): float
{
return $this->width;
}
}

// Класс адаптера позволяет вставлять квадратные колышки в
// круглые отверстия. Он расширяет класс RoundPeg, чтобы
// объекты адаптера могли выступать в роли круглых колышков.
class SquarePegAdapter extends RoundPeg
{
// На самом деле адаптер содержит экземпляр класса
// SquarePeg.
public function __construct(private SquarePeg $peg)
{
parent::__construct(0);
}

public function getRadius(): float
{
// Адаптер притворяется круглым колышком с радиусом,
// достаточным для того, чтобы вместить квадратный
// колышек, который адаптер фактически оборачивает.
return $this->peg->getWidth() * sqrt(2) / 2;
}
}

// Где-то в клиентском коде.
$hole = new RoundHole(5);
$rpeg = new RoundPeg(5);
var_dump($hole->fits($rpeg)); // true

$smallSqPeg = new SquarePeg(5);
$largeSqPeg = new SquarePeg(10);

$smallSqPegAdapter = new SquarePegAdapter($smallSqPeg);
$largeSqPegAdapter = new SquarePegAdapter($largeSqPeg);
var_dump($hole->fits($smallSqPegAdapter)); // true
var_dump($hole->fits($largeSqPegAdapter)); // false

Что такое паттерн Bridge?​

Middle

Ответ: Bridge — это структурный паттерн проектирования, который позволяет разделить большой класс или набор тесно связанных классов на две отдельные иерархии — абстракцию и реализацию, — которые могут развиваться независимо друг от друга.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете создавать платформонезависимые классы и приложения. Клиентский код работает с высокоуровневыми абстракциями. Он не взаимодействует с деталями платформы напрямую. Принцип открытости/закрытости (OCP). Вы можете вводить новые абстракции и реализации независимо друг от друга. Принцип единственной ответственности (SRP). Вы можете сосредоточиться на высокоуровневой логике в абстракции и на деталях платформы в реализации.
  • Минусы: Вы можете усложнить код, применяя паттерн к и без того сильно связанному классу.

Пример:

<?php

// «Абстракция» определяет интерфейс для «управляющей» части
// двух иерархий классов. Она хранит ссылку на объект иерархии
// «реализации» и делегирует ему всю реальную работу.
class RemoteControl
{
public function __construct(protected Device $device)
{
}

public function togglePower(): void
{
if ($this->device->isEnabled()) {
$this->device->disable();
} else {
$this->device->enable();
}
}

public function volumeDown(): void
{
$this->device->setVolume($this->device->getVolume() - 10);
}

public function volumeUp(): void
{
$this->device->setVolume($this->device->getVolume() + 10);
}

public function channelDown(): void
{
$this->device->setChannel($this->device->getChannel() - 1);
}

public function channelUp(): void
{
$this->device->setChannel($this->device->getChannel() + 1);
}
}

// Вы можете расширять классы из иерархии абстракции независимо
// от классов устройств.
class AdvancedRemoteControl extends RemoteControl
{
public function mute(): void
{
$this->device->setVolume(0);
}
}

// Интерфейс «реализации» объявляет методы, общие для всех
// конкретных классов реализации. Он не обязан совпадать с
// интерфейсом абстракции. На практике оба интерфейса могут быть
// совершенно различными. Обычно интерфейс реализации
// предоставляет только примитивные операции, тогда как
// абстракция определяет операции более высокого уровня на
// основе этих примитивов.
interface Device
{
public function isEnabled(): bool;
public function enable(): void;
public function disable(): void;
public function getVolume(): int;
public function setVolume(int $percent): void;
public function getChannel(): int;
public function setChannel(int $channel): void;
}

abstract class BaseDevice implements Device
{
protected bool $enabled = false;
protected int $volume = 30;
protected int $channel = 1;

public function isEnabled(): bool
{
return $this->enabled;
}

public function enable(): void
{
$this->enabled = true;
}

public function disable(): void
{
$this->enabled = false;
}

public function getVolume(): int
{
return $this->volume;
}

public function setVolume(int $percent): void
{
$this->volume = max(0, min(100, $percent));
}

public function getChannel(): int
{
return $this->channel;
}

public function setChannel(int $channel): void
{
$this->channel = max(1, $channel);
}
}

// Все устройства следуют одному и тому же интерфейсу.
class Tv extends BaseDevice
{
}

class Radio extends BaseDevice
{
}

// Где-то в клиентском коде.
$tv = new Tv();
$remote = new RemoteControl($tv);
$remote->togglePower();

$radio = new Radio();
$advancedRemote = new AdvancedRemoteControl($radio);
$advancedRemote->togglePower();
$advancedRemote->mute();

Что такое паттерн Composite?​

Middle

Ответ: Composite — это структурный паттерн проектирования, который позволяет компоновать объекты в древовидные структуры и затем работать с этими структурами так, как если бы они были отдельными объектами.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете удобнее работать со сложными древовидными структурами: используя полиморфизм и рекурсию в своих интересах. Принцип открытости/закрытости (OCP). Вы можете вводить новые типы элементов в приложение, не нарушая существующий код, работающий с деревом объектов.
  • Минусы: Может оказаться сложным предоставить общий интерфейс для классов, функциональность которых существенно различается. В некоторых сценариях потребуется чрезмерное обобщение интерфейса компонента, что затрудняет его понимание.

Пример:

<?php

// Интерфейс компонента объявляет общие операции как для
// простых, так и для сложных объектов композиции.
interface Graphic
{
public function move(int $x, int $y): void;
public function draw(): void;
}

// Класс листа представляет конечные объекты композиции. Объект
// лист не может иметь подобъектов. Обычно именно объекты-листья
// выполняют реальную работу, тогда как составные объекты лишь
// делегируют работу своим подкомпонентам.
class Dot implements Graphic
{
public function __construct(protected int $x, protected int $y)
{
}

public function move(int $x, int $y): void
{
$this->x += $x;
$this->y += $y;
}

public function draw(): void
{
echo "Draw a dot at ({$this->x}, {$this->y}).\n";
}
}

// Все классы компонентов могут расширять другие компоненты.
class Circle extends Dot
{
public function __construct(int $x, int $y, private int $radius)
{
parent::__construct($x, $y);
}

public function draw(): void
{
echo "Draw a circle at ({$this->x}, {$this->y}) with radius {$this->radius}.\n";
}
}

// Класс составного объекта представляет сложные компоненты,
// которые могут содержать дочерние элементы. Составные объекты
// обычно делегируют реальную работу своим дочерним элементам, а
// затем «суммируют» результат.
class CompoundGraphic implements Graphic
{
/** @var Graphic[] */
private array $children = [];

// Составной объект может добавлять или удалять другие
// компоненты (как простые, так и сложные) из своего
// списка дочерних элементов.
public function add(Graphic $child): void
{
$this->children[] = $child;
}

public function remove(Graphic $child): void
{
foreach ($this->children as $index => $component) {
if ($component === $child) {
unset($this->children[$index]);
}
}

$this->children = array_values($this->children);
}

public function move(int $x, int $y): void
{
foreach ($this->children as $child) {
$child->move($x, $y);
}
}

// Составной объект выполняет свою основную логику
// определённым образом. Он рекурсивно обходит все свои
// дочерние элементы, собирая и суммируя их результаты.
// Поскольку дочерние элементы составного объекта передают
// эти вызовы своим собственным дочерним элементам и так
// далее, в результате обходится всё дерево объектов.
public function draw(): void
{
foreach ($this->children as $child) {
$child->draw();
}

echo "Draw a dashed bounding rectangle around all child graphics.\n";
}
}

// Клиентский код работает со всеми компонентами через их
// базовый интерфейс. Таким образом, клиентский код может
// поддерживать как простые компоненты-листья, так и сложные
// составные объекты.
class ImageEditor
{
private CompoundGraphic $all;

public function load(): void
{
$this->all = new CompoundGraphic();
$this->all->add(new Dot(1, 2));
$this->all->add(new Circle(5, 3, 10));
}

// Объединяем выбранные компоненты в один сложный составной
// компонент.
public function groupSelected(array $components): void
{
$group = new CompoundGraphic();

foreach ($components as $component) {
if (!$component instanceof Graphic) {
continue;
}

$group->add($component);
$this->all->remove($component);
}

$this->all->add($group);
// Все компоненты будут отрисованы.
$this->all->draw();
}
}

$editor = new ImageEditor();
$editor->load();
$editor->groupSelected([new Dot(0, 0)]);

Что такое паттерн Decorator?​

Middle

Ответ: Decorator — это структурный паттерн проектирования, который позволяет добавлять объектам новое поведение, помещая их в специальные объекты-обёртки, содержащие это поведение.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете расширять поведение объекта, не создавая новый подкласс. Вы можете добавлять или удалять обязанности объекта во время выполнения. Вы можете комбинировать несколько вариантов поведения, оборачивая объект в несколько декораторов. Принцип единственной ответственности (SRP). Вы можете разделить монолитный класс, реализующий множество возможных вариантов поведения, на несколько более мелких классов.
  • Минусы: Трудно удалить конкретную обёртку из стека обёрток. Трудно реализовать декоратор так, чтобы его поведение не зависело от порядка в стеке декораторов. Начальный код конфигурации слоёв может выглядеть довольно неприглядно.

Пример:

<?php

// Интерфейс компонента определяет операции, которые могут быть
// изменены декораторами.
interface DataSource
{
public function writeData(string $data): void;
public function readData(): string;
}

// Конкретные компоненты предоставляют реализации операций по
// умолчанию. В программе может быть несколько вариантов этих
// классов.
class FileDataSource implements DataSource
{
/** @var array<string, string> */
private static array $storage = [];

public function __construct(private string $filename)
{
}

public function writeData(string $data): void
{
self::$storage[$this->filename] = $data;
}

public function readData(): string
{
return self::$storage[$this->filename] ?? '';
}
}

// Базовый класс декоратора следует тому же интерфейсу, что и
// другие компоненты. Основная задача этого класса — определить
// интерфейс обёртки для всех конкретных декораторов. Реализация
// кода обёртки по умолчанию может включать поле для хранения
// оборачиваемого компонента и средства его инициализации.
class DataSourceDecorator implements DataSource
{
public function __construct(protected DataSource $wrappee)
{
}

// Базовый декоратор просто делегирует всю работу
// оборачиваемому компоненту. Дополнительное поведение
// можно добавить в конкретных декораторах.
public function writeData(string $data): void
{
$this->wrappee->writeData($data);
}

// Конкретные декораторы могут вызывать родительскую
// реализацию операции вместо прямого обращения к
// оборачиваемому объекту. Такой подход упрощает расширение
// классов декораторов.
public function readData(): string
{
return $this->wrappee->readData();
}
}

// Конкретные декораторы должны вызывать методы оборачиваемого
// объекта, но могут добавлять к результату что-то своё.
// Декораторы могут выполнять добавленное поведение как до, так
// и после вызова оборачиваемого объекта.
class EncryptionDecorator extends DataSourceDecorator
{
public function writeData(string $data): void
{
parent::writeData(base64_encode($data));
}

public function readData(): string
{
return (string) base64_decode(parent::readData(), true);
}
}

// Вы можете оборачивать объекты в несколько слоёв декораторов.
class CompressionDecorator extends DataSourceDecorator
{
public function writeData(string $data): void
{
parent::writeData(base64_encode(gzcompress($data)));
}

public function readData(): string
{
$decoded = (string) base64_decode(parent::readData(), true);
return (string) gzuncompress($decoded);
}
}

// Вариант 1. Простой пример сборки декораторов.
class Application
{
public function dumbUsageExample(string $salaryRecords): void
{
$source = new FileDataSource('somefile.dat');
$source->writeData($salaryRecords);
// Целевой файл записан с обычными данными.

$source = new CompressionDecorator($source);
$source->writeData($salaryRecords);
// Целевой файл записан с сжатыми данными.

$source = new EncryptionDecorator($source);
// Переменная source теперь содержит:
// Encryption > Compression > FileDataSource
$source->writeData($salaryRecords);
// Файл записан с сжатыми и зашифрованными данными.
}
}

// Вариант 2. Клиентский код, использующий внешний источник
// данных. Объекты SalaryManager не знают и не заботятся о
// специфике хранения данных. Они работают с предварительно
// сконфигурированным источником данных, полученным от
// конфигуратора приложения.
class SalaryManager
{
public function __construct(private DataSource $source)
{
}

public function load(): string
{
return $this->source->readData();
}

public function save(string $salaryRecords): void
{
$this->source->writeData($salaryRecords);
}
}

// Приложение может собирать различные стеки декораторов во
// время выполнения, в зависимости от конфигурации или среды.
class ApplicationConfigurator
{
public function configurationExample(bool $enabledEncryption, bool $enabledCompression): string
{
$source = new FileDataSource('salary.dat');
if ($enabledEncryption) {
$source = new EncryptionDecorator($source);
}
if ($enabledCompression) {
$source = new CompressionDecorator($source);
}

$manager = new SalaryManager($source);
$manager->save('Alice: 5000; Bob: 6000');

return $manager->load();
}
}

$configurator = new ApplicationConfigurator();
echo $configurator->configurationExample(true, true);

Что такое паттерн Facade?​

Middle

Ответ: Facade — это структурный паттерн проектирования, который предоставляет упрощённый интерфейс к библиотеке, фреймворку или любому другому сложному набору классов.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете изолировать свой код от сложности подсистемы.
  • Минусы: Фасад может превратиться в god object, связанный со всеми классами приложения.

Пример:

<?php

// Это некоторые классы сложного стороннего фреймворка для
// конвертации видео. Мы не контролируем этот код, поэтому не
// можем его упростить.
class VideoFile
{
public function __construct(public string $name)
{
}
}

class OggCompressionCodec
{
}

class MPEG4CompressionCodec
{
}

class CodecFactory
{
public function extract(VideoFile $file): object
{
return str_ends_with($file->name, '.ogg') ? new OggCompressionCodec() : new MPEG4CompressionCodec();
}
}

class BitrateReader
{
public static function read(string $filename, object $codec): string
{
return "raw({$filename})";
}

public static function convert(string $buffer, object $codec): string
{
return "converted({$buffer})";
}
}

class AudioMixer
{
public function fix(string $result): string
{
return "audio-fixed({$result})";
}
}

class ConvertedFile
{
public function __construct(private string $content)
{
}

public function save(): void
{
echo "Saving converted file: {$this->content}\n";
}
}

// Мы создаём класс фасада, чтобы скрыть сложность фреймворка
// за простым интерфейсом. Это компромисс между
// функциональностью и простотой.
class VideoConverter
{
public function convert(string $filename, string $format): ConvertedFile
{
$file = new VideoFile($filename);
$sourceCodec = (new CodecFactory())->extract($file);

if ($format === 'mp4') {
$destinationCodec = new MPEG4CompressionCodec();
} else {
$destinationCodec = new OggCompressionCodec();
}

$buffer = BitrateReader::read($filename, $sourceCodec);
$result = BitrateReader::convert($buffer, $destinationCodec);
$result = (new AudioMixer())->fix($result);

return new ConvertedFile($result);
}
}

// Классы приложения не зависят от миллиарда классов,
// предоставляемых сложным фреймворком. Кроме того, если вы
// решите сменить фреймворк, вам нужно будет переписать только
// класс фасада.
class Application
{
public function main(): void
{
$converter = new VideoConverter();
$mp4 = $converter->convert('funny-cats-video.ogg', 'mp4');
$mp4->save();
}
}

$app = new Application();
$app->main();

Что такое паттерн Flyweight?​

Middle

Ответ: Flyweight — это структурный паттерн проектирования, который позволяет вместить большее количество объектов в доступный объём оперативной памяти за счёт совместного использования общих частей состояния между множеством объектов вместо хранения всех данных в каждом объекте.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете сэкономить много оперативной памяти при условии, что в вашей программе множество похожих объектов.
  • Минусы: Вы можете жертвовать циклами процессора в пользу экономии памяти, когда некоторые контекстные данные нужно пересчитывать при каждом вызове метода Flyweight. Код становится значительно сложнее. Новые члены команды будут постоянно задаваться вопросом, почему состояние сущности разделено именно таким образом.

Пример:

<?php

// Класс Flyweight содержит часть состояния дерева. Эти поля
// хранят значения, уникальные для каждого конкретного дерева.
// Например, здесь вы не найдёте координат дерева. Но текстура
// и цвета, общие для множества деревьев, находятся здесь.
// Поскольку эти данные обычно БОЛЬШИЕ, вы бы тратили много
// памяти, храня их в каждом объекте дерева. Вместо этого мы
// можем извлечь текстуру, цвет и другие повторяющиеся данные в
// отдельный объект, на который могут ссылаться множество
// отдельных объектов деревьев.
class TreeType
{
public function __construct(
private string $name,
private string $color,
private string $texture
) {
}

public function draw(string $canvas, int $x, int $y): void
{
echo "Draw {$this->name} ({$this->color}, {$this->texture}) on {$canvas} at ({$x}, {$y}).\n";
}
}

// Фабрика Flyweight решает, следует ли повторно использовать
// существующий Flyweight или создать новый объект.
class TreeFactory
{
/** @var array<string, TreeType> */
private static array $treeTypes = [];

public static function getTreeType(string $name, string $color, string $texture): TreeType
{
$key = strtolower("{$name}|{$color}|{$texture}");

if (!isset(self::$treeTypes[$key])) {
self::$treeTypes[$key] = new TreeType($name, $color, $texture);
}

return self::$treeTypes[$key];
}
}

// Контекстный объект содержит внешнюю часть состояния дерева.
// Приложение может создавать миллиарды таких объектов, поскольку
// они довольно маленькие: всего два целочисленных поля
// координат и одно поле-ссылка.
class Tree
{
public function __construct(private int $x, private int $y, private TreeType $type)
{
}

public function draw(string $canvas): void
{
$this->type->draw($canvas, $this->x, $this->y);
}
}

// Классы Tree и Forest являются клиентами Flyweight. Вы можете
// объединить их, если не планируете дальнейшее развитие класса
// Tree.
class Forest
{
/** @var Tree[] */
private array $trees = [];

public function plantTree(int $x, int $y, string $name, string $color, string $texture): void
{
$type = TreeFactory::getTreeType($name, $color, $texture);
$tree = new Tree($x, $y, $type);
$this->trees[] = $tree;
}

public function draw(string $canvas): void
{
foreach ($this->trees as $tree) {
$tree->draw($canvas);
}
}
}

$forest = new Forest();
$forest->plantTree(1, 2, 'Oak', 'Green', 'Rough');
$forest->plantTree(3, 4, 'Oak', 'Green', 'Rough');
$forest->plantTree(5, 6, 'Pine', 'Dark Green', 'Needle');
$forest->draw('MainCanvas');

Что такое паттерн Proxy?​

Middle

Ответ: Proxy — это структурный паттерн проектирования, который позволяет предоставить заменитель или заполнитель для другого объекта. Proxy контролирует доступ к исходному объекту, позволяя выполнить что-то до или после того, как запрос достигнет исходного объекта.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете контролировать сервисный объект незаметно для клиентов. Вы можете управлять жизненным циклом сервисного объекта, когда клиентам это безразлично. Proxy работает, даже если сервисный объект ещё не готов или недоступен. Принцип открытости/закрытости (OCP). Вы можете вводить новые прокси, не изменяя сервис или клиентов.
  • Минусы: Код может стать сложнее, поскольку необходимо ввести множество новых классов. Ответ от сервиса может задержаться.

Пример:

<?php

// Интерфейс удалённого сервиса.
interface ThirdPartyYouTubeLib
{
/** @return array<string, string> */
public function listVideos(): array;
public function getVideoInfo(string $id): string;
public function downloadVideo(string $id): string;
}

// Конкретная реализация коннектора сервиса. Методы этого класса
// могут запрашивать информацию у YouTube. Скорость запроса
// зависит от интернет-соединения пользователя, а также от
// YouTube. Приложение замедлится, если одновременно будет
// отправлено много запросов, даже если все они запрашивают одну
// и ту же информацию.
class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib
{
public function listVideos(): array
{
return ['a1' => 'Funny Cats', 'b2' => 'PHP Patterns'];
}

public function getVideoInfo(string $id): string
{
return "Video info for {$id}";
}

public function downloadVideo(string $id): string
{
return "Downloading video {$id}";
}
}

// Для экономии трафика мы можем кэшировать результаты
// запросов и хранить их некоторое время. Но может оказаться
// невозможным поместить такой код непосредственно в класс
// сервиса. Например, он мог быть предоставлен как часть
// сторонней библиотеки и/или определён как `final`. Поэтому мы
// помещаем код кэширования в новый класс-прокси, реализующий
// тот же интерфейс, что и класс сервиса. Он делегирует
// сервисному объекту только тогда, когда действительно нужно
// отправить реальный запрос.
class CachedYouTubeClass implements ThirdPartyYouTubeLib
{
/** @var array<string, string>|null */
private ?array $listCache = null;

/** @var array<string, string> */
private array $videoCache = [];

/** @var array<string, string> */
private array $downloadCache = [];

public bool $needReset = false;

public function __construct(private ThirdPartyYouTubeLib $service)
{
}

public function listVideos(): array
{
if ($this->listCache === null || $this->needReset) {
$this->listCache = $this->service->listVideos();
$this->needReset = false;
}

return $this->listCache;
}

public function getVideoInfo(string $id): string
{
if (!isset($this->videoCache[$id]) || $this->needReset) {
$this->videoCache[$id] = $this->service->getVideoInfo($id);
$this->needReset = false;
}

return $this->videoCache[$id];
}

public function downloadVideo(string $id): string
{
if (!isset($this->downloadCache[$id]) || $this->needReset) {
$this->downloadCache[$id] = $this->service->downloadVideo($id);
$this->needReset = false;
}

return $this->downloadCache[$id];
}
}

// Класс GUI, который ранее работал напрямую с объектом
// сервиса, остаётся неизменным, пока он работает с сервисным
// объектом через интерфейс. Мы можем безопасно передать
// объект-прокси вместо реального сервисного объекта, поскольку
// оба реализуют один и тот же интерфейс.
class YouTubeManager
{
public function __construct(protected ThirdPartyYouTubeLib $service)
{
}

public function renderVideoPage(string $id): void
{
$info = $this->service->getVideoInfo($id);
echo "Render page: {$info}\n";
}

public function renderListPanel(): void
{
$list = $this->service->listVideos();
echo 'Render list: ' . implode(', ', $list) . "\n";
}

public function reactOnUserInput(): void
{
$this->renderVideoPage('a1');
$this->renderListPanel();
}
}

// Приложение может настраивать прокси на лету.
class Application
{
public function init(): void
{
$youTubeService = new ThirdPartyYouTubeClass();
$youTubeProxy = new CachedYouTubeClass($youTubeService);
$manager = new YouTubeManager($youTubeProxy);
$manager->reactOnUserInput();
}
}

$app = new Application();
$app->init();

Что такое паттерн Chain of Responsibility?​

Middle

Ответ: Chain of Responsibility — это поведенческий паттерн проектирования, который позволяет передавать запросы по цепочке обработчиков. При получении запроса каждый обработчик решает, обработать его или передать следующему обработчику в цепочке.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете контролировать порядок обработки запросов. Принцип единственной ответственности (SRP). Вы можете отделить классы, инициирующие операции, от классов, выполняющих операции. Принцип открытости/закрытости (OCP). Вы можете вводить новые обработчики в приложение, не нарушая существующий клиентский код.
  • Минусы: Некоторые запросы могут остаться необработанными.

Пример:

<?php

// Интерфейс обработчика объявляет метод для выполнения
// запроса.
interface ComponentWithContextualHelp
{
public function showHelp(): void;
}

// Базовый класс для простых компонентов.
abstract class Component implements ComponentWithContextualHelp
{
public ?string $tooltipText = null;

// Контейнер компонента выступает следующим звеном в
// цепочке обработчиков.
protected ?Container $container = null;

public function setContainer(Container $container): void
{
$this->container = $container;
}

// Компонент показывает всплывающую подсказку, если ему
// назначен текст помощи. В противном случае он передаёт
// вызов контейнеру, если тот существует.
public function showHelp(): void
{
if ($this->tooltipText !== null) {
echo "Tooltip: {$this->tooltipText}\n";
return;
}

if ($this->container !== null) {
$this->container->showHelp();
}
}
}

// Контейнеры могут содержать как простые компоненты, так и
// другие контейнеры в качестве дочерних элементов. Цепочные
// связи устанавливаются здесь. Класс наследует поведение
// showHelp от своего родителя.
abstract class Container extends Component
{
/** @var Component[] */
protected array $children = [];

public function add(Component $child): void
{
$this->children[] = $child;
$child->setContainer($this);
}
}

// Примитивные компоненты могут быть удовлетворены реализацией
// помощи по умолчанию...
class Button extends Component
{
public function __construct(private string $label)
{
}
}

// Но сложные компоненты могут переопределить реализацию по
// умолчанию. Если текст помощи не может быть предоставлен
// новым способом, компонент всегда может вызвать базовую
// реализацию (см. класс Component).
class Panel extends Container
{
public ?string $modalHelpText = null;

public function showHelp(): void
{
if ($this->modalHelpText !== null) {
echo "Modal: {$this->modalHelpText}\n";
return;
}

parent::showHelp();
}
}

// ...аналогично предыдущему...
class Dialog extends Container
{
public ?string $wikiPageURL = null;

public function __construct(private string $title)
{
}

public function showHelp(): void
{
if ($this->wikiPageURL !== null) {
echo "Open wiki page: {$this->wikiPageURL}\n";
return;
}

parent::showHelp();
}
}

// Клиентский код.
class Application
{
private Component $focusedComponent;

// Каждое приложение настраивает цепочку по-своему.
public function createUI(): void
{
$dialog = new Dialog('Budget Reports');
$dialog->wikiPageURL = 'http://help.local/budget-reports';

$panel = new Panel();
$panel->modalHelpText = 'This panel helps you manage budget data.';

$ok = new Button('OK');
$ok->tooltipText = 'This is an OK button that saves changes.';
$cancel = new Button('Cancel');

$panel->add($ok);
$panel->add($cancel);
$dialog->add($panel);

$this->focusedComponent = $cancel;
}

// Представьте, что здесь происходит.
public function onF1KeyPress(): void
{
$component = $this->getComponentAtMouseCoords();
$component->showHelp();
}

private function getComponentAtMouseCoords(): Component
{
return $this->focusedComponent;
}
}

$app = new Application();
$app->createUI();
$app->onF1KeyPress();

Что такое паттерн Command?​

Middle

Ответ: Command — это поведенческий паттерн проектирования, который превращает запрос в самостоятельный объект, содержащий всю информацию о запросе. Такое преобразование позволяет передавать запросы в качестве аргументов методов, откладывать или ставить в очередь выполнение запроса, а также поддерживать отменяемые операции.

Типичное использование / Плюсы и минусы:

  • Плюсы: Принцип единственной ответственности (SRP). Вы можете отделить классы, инициирующие операции, от классов, выполняющих эти операции. Принцип открытости/закрытости (OCP). Вы можете вводить новые команды в приложение, не нарушая существующий клиентский код. Вы можете реализовать отмену/повтор. Вы можете реализовать отложенное выполнение операций. Вы можете собрать набор простых команд в сложную.
  • Минусы: Код может стать сложнее, поскольку вы вводите целый новый слой между отправителями и получателями.

Пример:

<?php

// Базовый класс команды определяет общий интерфейс для всех
// конкретных команд.
abstract class Command
{
protected string $backup = '';

public function __construct(protected Application $app, protected Editor $editor)
{
}

// Создаём резервную копию состояния редактора.
protected function saveBackup(): void
{
$this->backup = $this->editor->text;
}

// Восстанавливаем состояние редактора.
public function undo(): void
{
$this->editor->text = $this->backup;
}

// Метод выполнения объявлен абстрактным, чтобы заставить
// все конкретные команды предоставлять собственные
// реализации. Метод должен возвращать true или false в
// зависимости от того, изменяет ли команда состояние
// редактора.
abstract public function execute(): bool;
}

// Конкретные команды описаны ниже.
class CopyCommand extends Command
{
// Команда копирования не сохраняется в историю, поскольку
// она не изменяет состояние редактора.
public function execute(): bool
{
$this->app->clipboard = $this->editor->getSelection();
return false;
}
}

class CutCommand extends Command
{
// Команда вырезания изменяет состояние редактора, поэтому
// её нужно сохранить в историю. И она будет сохранена, пока
// метод возвращает true.
public function execute(): bool
{
$this->saveBackup();
$this->app->clipboard = $this->editor->getSelection();
$this->editor->deleteSelection();
return true;
}
}

class PasteCommand extends Command
{
public function execute(): bool
{
$this->saveBackup();
$this->editor->replaceSelection($this->app->clipboard);
return true;
}
}

// Операция отмены тоже является командой.
class UndoCommand extends Command
{
public function execute(): bool
{
$this->app->undo();
return false;
}
}

// Глобальная история команд — это просто стек.
class CommandHistory
{
/** @var Command[] */
private array $history = [];

// Последний вошёл...
public function push(Command $command): void
{
$this->history[] = $command;
}

// ...первый вышел
public function pop(): ?Command
{
return array_pop($this->history);
}
}

// Класс редактора содержит реальные операции редактирования
// текста. Он играет роль получателя: все команды в конечном
// итоге делегируют выполнение методам редактора.
class Editor
{
public string $text = '';
private int $selectionStart = 0;
private int $selectionLength = 0;

public function setSelection(int $start, int $length): void
{
$this->selectionStart = max(0, $start);
$this->selectionLength = max(0, $length);
}

public function getSelection(): string
{
return substr($this->text, $this->selectionStart, $this->selectionLength);
}

public function deleteSelection(): void
{
$this->text = substr_replace($this->text, '', $this->selectionStart, $this->selectionLength);
$this->selectionLength = 0;
}

public function replaceSelection(string $text): void
{
$this->text = substr_replace($this->text, $text, $this->selectionStart, $this->selectionLength);
$this->selectionLength = strlen($text);
}
}

// Класс приложения настраивает связи между объектами. Он
// выступает в роли отправителя: когда нужно что-то сделать, он
// создаёт объект команды и выполняет его.
class Application
{
public string $clipboard = '';

/** @var Editor[] */
public array $editors = [];

public Editor $activeEditor;
public CommandHistory $history;

public function __construct()
{
$this->history = new CommandHistory();
}

// Код, назначающий команды элементам UI, может выглядеть
// так.
public function createUI(): void
{
$editor = new Editor();
$editor->text = 'Hello Design Patterns';
$editor->setSelection(6, 6); // "Design"
$this->activeEditor = $editor;
$this->editors[] = $editor;

$copy = fn () => $this->executeCommand(new CopyCommand($this, $this->activeEditor));
$cut = fn () => $this->executeCommand(new CutCommand($this, $this->activeEditor));
$paste = fn () => $this->executeCommand(new PasteCommand($this, $this->activeEditor));
$undo = fn () => $this->executeCommand(new UndoCommand($this, $this->activeEditor));

$copy();
$cut();
$this->activeEditor->setSelection(0, 0);
$paste();
$undo();

echo $this->activeEditor->text . "\n";
}

// Выполняем команду и проверяем, нужно ли добавить её в
// историю.
public function executeCommand(Command $command): void
{
if ($command->execute()) {
$this->history->push($command);
}
}

// Берём последнюю команду из истории и вызываем её метод
// undo. Обратите внимание, что мы не знаем класс этой
// команды. Но нам это и не нужно, поскольку команда сама
// знает, как отменить своё действие.
public function undo(): void
{
$command = $this->history->pop();
if ($command !== null) {
$command->undo();
}
}
}

$app = new Application();
$app->createUI();

Что такое паттерн Mediator?​

Middle

Ответ: Mediator — это поведенческий паттерн проектирования, который позволяет уменьшить хаотичные зависимости между объектами. Паттерн ограничивает прямые коммуникации между объектами и заставляет их взаимодействовать только через объект-посредник.

Типичное использование / Плюсы и минусы:

  • Плюсы: Принцип единственной ответственности (SRP). Вы можете извлечь коммуникации между различными компонентами в одно место, что упрощает их понимание и поддержку. Принцип открытости/закрытости (OCP). Вы можете вводить новые посредники без необходимости изменять сами компоненты. Вы можете уменьшить связанность между различными компонентами программы. Вы можете легче повторно использовать отдельные компоненты.
  • Минусы: Со временем посредник может эволюционировать в God Object.

Пример:

<?php

// Интерфейс посредника объявляет метод, используемый
// компонентами для уведомления посредника о различных событиях.
// Посредник может реагировать на эти события и передавать
// выполнение другим компонентам.
interface Mediator
{
public function notify(Component $sender, string $event): void;
}

// Конкретный класс посредника. Запутанная сеть связей между
// отдельными компонентами была распутана и перенесена в
// посредник.
class AuthenticationDialog implements Mediator
{
private string $title = 'Log in';
private Checkbox $loginOrRegisterChkBx;
private Textbox $loginUsername;
private Textbox $loginPassword;
private Textbox $registrationUsername;
private Textbox $registrationPassword;
private Textbox $registrationEmail;
private Button $okBtn;
private Button $cancelBtn;

public function __construct()
{
// Создаём все объекты компонентов, передавая текущий
// посредник в их конструкторы для установки связей.
$this->loginOrRegisterChkBx = new Checkbox($this);
$this->loginUsername = new Textbox($this);
$this->loginPassword = new Textbox($this);
$this->registrationUsername = new Textbox($this);
$this->registrationPassword = new Textbox($this);
$this->registrationEmail = new Textbox($this);
$this->okBtn = new Button($this, 'OK');
$this->cancelBtn = new Button($this, 'Cancel');
}

// Когда что-то происходит с компонентом, он уведомляет
// посредник. Получив уведомление, посредник может выполнить
// что-то самостоятельно или передать запрос другому
// компоненту.
public function notify(Component $sender, string $event): void
{
if ($sender === $this->loginOrRegisterChkBx && $event === 'check') {
if ($this->loginOrRegisterChkBx->isChecked()) {
$this->title = 'Log in';
echo "Show login form; hide registration form.\n";
} else {
$this->title = 'Register';
echo "Show registration form; hide login form.\n";
}
}

if ($sender === $this->okBtn && $event === 'click') {
if ($this->loginOrRegisterChkBx->isChecked()) {
echo "Try to find a user using login credentials.\n";
} else {
echo "Create a user account and log that user in.\n";
}
}
}

public function demo(): void
{
$this->loginOrRegisterChkBx->setChecked(false);
$this->loginOrRegisterChkBx->check();
$this->okBtn->click();
}
}

// Компоненты взаимодействуют с посредником через интерфейс
// посредника. Благодаря этому вы можете использовать те же
// компоненты в других контекстах, связывая их с другими
// объектами-посредниками.
abstract class Component
{
public function __construct(protected Mediator $dialog)
{
}

public function click(): void
{
$this->dialog->notify($this, 'click');
}

public function keypress(): void
{
$this->dialog->notify($this, 'keypress');
}
}

// Конкретные компоненты не общаются друг с другом. У них есть
// только один канал связи — отправка уведомлений посреднику.
class Button extends Component
{
public function __construct(Mediator $dialog, private string $label)
{
parent::__construct($dialog);
}
}

class Textbox extends Component
{
public string $text = '';
}

class Checkbox extends Component
{
private bool $checked = true;

public function setChecked(bool $checked): void
{
$this->checked = $checked;
}

public function isChecked(): bool
{
return $this->checked;
}

public function check(): void
{
$this->dialog->notify($this, 'check');
}
}

$dialog = new AuthenticationDialog();
$dialog->demo();

Что такое паттерн Iterator?​

Middle

Ответ: Iterator — это поведенческий паттерн проектирования, который позволяет обходить элементы коллекции, не раскрывая её внутреннее представление (список, стек, дерево и т. д.).

Типичное использование / Плюсы и минусы:

  • Плюсы: Принцип единственной ответственности (SRP). Вы можете очистить клиентский код и коллекции, вынеся громоздкие алгоритмы обхода в отдельные классы. Принцип открытости/закрытости (OCP). Вы можете реализовать новые типы коллекций и итераторов и передавать их существующему коду, ничего не ломая. Вы можете итерировать одну и ту же коллекцию параллельно, поскольку каждый объект итератора содержит собственное состояние итерации. По той же причине вы можете отложить итерацию и продолжить её при необходимости.
  • Минусы: Применение паттерна может быть избыточным, если ваше приложение работает только с простыми коллекциями. Использование итератора может быть менее эффективным, чем прямой обход элементов некоторых специализированных коллекций.

Пример:

<?php

class Profile
{
public function __construct(private string $id, private string $email)
{
}

public function getId(): string
{
return $this->id;
}

public function getEmail(): string
{
return $this->email;
}
}

// Интерфейс коллекции должен объявить фабричный метод для
// создания итераторов. Вы можете объявить несколько методов,
// если в вашей программе доступны разные виды итерации.
interface SocialNetwork
{
public function createFriendsIterator(string $profileId): ProfileIterator;
public function createCoworkersIterator(string $profileId): ProfileIterator;
}

// Каждая конкретная коллекция связана с набором конкретных
// классов итераторов, которые она возвращает. Но клиент этого
// не знает, поскольку сигнатуры этих методов возвращают
// интерфейсы итераторов.
class Facebook implements SocialNetwork
{
/** @var array<string, array<string, Profile[]>> */
private array $graph;

public function __construct()
{
$this->graph = [
'john' => [
'friends' => [new Profile('amy', '[email protected]')],
'coworkers' => [new Profile('bob', '[email protected]')],
],
];
}

public function createFriendsIterator(string $profileId): ProfileIterator
{
return new FacebookIterator($this, $profileId, 'friends');
}

public function createCoworkersIterator(string $profileId): ProfileIterator
{
return new FacebookIterator($this, $profileId, 'coworkers');
}

/** @return Profile[] */
public function socialGraphRequest(string $profileId, string $type): array
{
return $this->graph[$profileId][$type] ?? [];
}
}

class LinkedIn implements SocialNetwork
{
public function createFriendsIterator(string $profileId): ProfileIterator
{
return new ArrayProfileIterator([new Profile('li_friend', '[email protected]')]);
}

public function createCoworkersIterator(string $profileId): ProfileIterator
{
return new ArrayProfileIterator([new Profile('li_coworker', '[email protected]')]);
}
}

// Общий интерфейс для всех итераторов.
interface ProfileIterator
{
public function getNext(): ?Profile;
public function hasMore(): bool;
}

// Конкретный класс итератора.
class FacebookIterator implements ProfileIterator
{
// Итератору нужна ссылка на коллекцию, которую он обходит.
private int $currentPosition = 0;

/** @var Profile[]|null */
private ?array $cache = null;

public function __construct(
private Facebook $facebook,
private string $profileId,
private string $type
) {
}

private function lazyInit(): void
{
if ($this->cache === null) {
$this->cache = $this->facebook->socialGraphRequest($this->profileId, $this->type);
}
}

// Каждый конкретный класс итератора имеет собственную
// реализацию общего интерфейса итератора.
public function getNext(): ?Profile
{
if ($this->hasMore()) {
return $this->cache[$this->currentPosition++];
}

return null;
}

public function hasMore(): bool
{
$this->lazyInit();
return $this->currentPosition < count($this->cache);
}
}

class ArrayProfileIterator implements ProfileIterator
{
private int $position = 0;

/** @param Profile[] $profiles */
public function __construct(private array $profiles)
{
}

public function getNext(): ?Profile
{
return $this->hasMore() ? $this->profiles[$this->position++] : null;
}

public function hasMore(): bool
{
return $this->position < count($this->profiles);
}
}

// Вот ещё один полезный приём: вы можете передать итератор
// клиентскому классу вместо предоставления ему доступа ко всей
// коллекции. Таким образом, вы не раскрываете коллекцию
// клиенту.
//
// И есть ещё одно преимущество: вы можете изменить способ
// работы клиента с коллекцией во время выполнения, передав ему
// другой итератор. Это возможно, потому что клиентский код не
// привязан к конкретным классам итераторов.
class SocialSpammer
{
public function send(ProfileIterator $iterator, string $message): void
{
while ($iterator->hasMore()) {
$profile = $iterator->getNext();
if ($profile !== null) {
echo "Send '{$message}' to {$profile->getEmail()}\n";
}
}
}
}

// Класс приложения настраивает коллекции и итераторы, а затем
// передаёт их клиентскому коду.
class Application
{
private SocialNetwork $network;
private SocialSpammer $spammer;

public function config(bool $useFacebook = true): void
{
$this->network = $useFacebook ? new Facebook() : new LinkedIn();
$this->spammer = new SocialSpammer();
}

public function sendSpamToFriends(Profile $profile): void
{
$iterator = $this->network->createFriendsIterator($profile->getId());
$this->spammer->send($iterator, 'Very important message');
}

public function sendSpamToCoworkers(Profile $profile): void
{
$iterator = $this->network->createCoworkersIterator($profile->getId());
$this->spammer->send($iterator, 'Very important message');
}
}

$app = new Application();
$app->config(true);
$profile = new Profile('john', '[email protected]');
$app->sendSpamToFriends($profile);
$app->sendSpamToCoworkers($profile);

Что такое паттерн Memento?​

Middle

Ответ: Memento — это поведенческий паттерн проектирования, который позволяет сохранять и восстанавливать предыдущее состояние объекта без раскрытия деталей его реализации.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете создавать снимки состояния объекта, не нарушая его инкапсуляцию. Вы можете упростить код хозяина, позволив смотрителю поддерживать историю состояний хозяина.
  • Минусы: Приложение может потреблять много оперативной памяти, если клиенты создают снимки слишком часто. Смотрители должны отслеживать жизненный цикл хозяина, чтобы иметь возможность уничтожать устаревшие снимки. Большинство динамических языков программирования, таких как PHP, Python и JavaScript, не могут гарантировать, что состояние внутри снимка останется нетронутым.

Пример:

<?php

// Хозяин (originator) хранит важные данные, которые могут
// изменяться со временем. Он также определяет метод для
// сохранения своего состояния в снимок и другой метод для
// восстановления состояния из снимка.
class Editor
{
private string $text = '';
private int $curX = 0;
private int $curY = 0;
private int $selectionWidth = 0;

public function setText(string $text): void
{
$this->text = $text;
}

public function setCursor(int $x, int $y): void
{
$this->curX = $x;
$this->curY = $y;
}

public function setSelectionWidth(int $width): void
{
$this->selectionWidth = $width;
}

// Сохраняет текущее состояние в снимок.
public function createSnapshot(): Snapshot
{
// Memento — это неизменяемый объект; поэтому хозяин
// передаёт своё состояние через параметры конструктора
// снимка.
return new Snapshot($this, $this->text, $this->curX, $this->curY, $this->selectionWidth);
}

public function dumpState(): string
{
return sprintf('text="%s", cursor=(%d,%d), selection=%d', $this->text, $this->curX, $this->curY, $this->selectionWidth);
}
}

// Класс снимка хранит прошлое состояние редактора.
class Snapshot
{
public function __construct(
private Editor $editor,
private string $text,
private int $curX,
private int $curY,
private int $selectionWidth
) {
}

// В какой-то момент предыдущее состояние редактора может
// быть восстановлено с помощью объекта снимка.
public function restore(): void
{
$this->editor->setText($this->text);
$this->editor->setCursor($this->curX, $this->curY);
$this->editor->setSelectionWidth($this->selectionWidth);
}
}

// Объект команды может выступать в роли смотрителя. В этом
// случае команда получает снимок непосредственно перед
// изменением состояния хозяина. Когда запрашивается отмена,
// она восстанавливает состояние хозяина из снимка.
class Command
{
private ?Snapshot $backup = null;

public function __construct(private Editor $editor)
{
}

public function makeBackup(): void
{
$this->backup = $this->editor->createSnapshot();
}

public function undo(): void
{
if ($this->backup !== null) {
$this->backup->restore();
}
}
}

$editor = new Editor();
$editor->setText('Hello');
$editor->setCursor(1, 1);
$editor->setSelectionWidth(3);

$command = new Command($editor);
$command->makeBackup();

$editor->setText('Changed');
$editor->setCursor(5, 5);

$command->undo();
echo $editor->dumpState();

Что такое паттерн Observer?​

Middle

Ответ: Observer — это поведенческий паттерн проектирования, который позволяет определить механизм подписки для уведомления множества объектов о любых событиях, происходящих с наблюдаемым объектом.

Типичное использование / Плюсы и минусы:

  • Плюсы: Принцип открытости/закрытости (OCP). Вы можете вводить новые классы подписчиков без необходимости изменять код издателя (и наоборот, если есть интерфейс издателя). Вы можете устанавливать связи между объектами во время выполнения.
  • Минусы: Подписчики уведомляются в случайном порядке.

Пример:

<?php

// Базовый класс издателя включает код управления подписками и
// методы уведомления.
class EventManager
{
/** @var array<string, EventListener[]> */
private array $listeners = [];

public function subscribe(string $eventType, EventListener $listener): void
{
$this->listeners[$eventType][] = $listener;
}

public function unsubscribe(string $eventType, EventListener $listener): void
{
if (!isset($this->listeners[$eventType])) {
return;
}

$this->listeners[$eventType] = array_values(
array_filter(
$this->listeners[$eventType],
fn (EventListener $current): bool => $current !== $listener
)
);
}

public function notify(string $eventType, string $data): void
{
foreach ($this->listeners[$eventType] ?? [] as $listener) {
$listener->update($data);
}
}
}

// Конкретный издатель содержит реальную бизнес-логику,
// интересную для некоторых подписчиков. Мы могли бы
// наследовать этот класс от базового издателя, но это не
// всегда возможно в реальной жизни, поскольку конкретный
// издатель уже может быть подклассом. В этом случае можно
// внедрить логику подписки через композицию, как мы и
// сделали здесь.
class Editor
{
public EventManager $events;
private ?string $file = null;

public function __construct()
{
$this->events = new EventManager();
}

// Методы бизнес-логики могут уведомлять подписчиков об
// изменениях.
public function openFile(string $path): void
{
$this->file = $path;
$this->events->notify('open', basename($path));
}

public function saveFile(): void
{
if ($this->file === null) {
return;
}

$this->events->notify('save', basename($this->file));
}
}

// Интерфейс подписчика. Если ваш язык программирования
// поддерживает функциональные типы, вы можете заменить всю
// иерархию подписчиков набором функций.
interface EventListener
{
public function update(string $filename): void;
}

// Конкретные подписчики реагируют на обновления, выпущенные
// издателем, к которому они подключены.
class LoggingListener implements EventListener
{
public function __construct(private string $logFilename, private string $message)
{
}

public function update(string $filename): void
{
echo "[{$this->logFilename}] " . sprintf($this->message, $filename) . "\n";
}
}

class EmailAlertsListener implements EventListener
{
public function __construct(private string $email, private string $message)
{
}

public function update(string $filename): void
{
echo "Email to {$this->email}: " . sprintf($this->message, $filename) . "\n";
}
}

// Приложение может настраивать издателей и подписчиков во
// время выполнения.
class Application
{
public function config(): void
{
$editor = new Editor();

$logger = new LoggingListener(
'/path/to/log.txt',
'Someone has opened the file: %s'
);
$editor->events->subscribe('open', $logger);

$emailAlerts = new EmailAlertsListener(
'Someone has changed the file: %s'
);
$editor->events->subscribe('save', $emailAlerts);

$editor->openFile('/tmp/test.txt');
$editor->saveFile();
}
}

$app = new Application();
$app->config();

SplObserver и SplSubject в SPL (Standard PHP Library), ссылка: https://www.php.net/manual/en/class.splobserver.php

<?php

/**
* SplObserver и SplSubject в SPL (Standard PHP Library)
* Пример из реальной жизни: модель User, которая уведомляет систему рассылки и логгер при регистрации пользователя.
*/

class User implements SplSubject
{
private string $email;
private SplObjectStorage $observers;

public function __construct(string $email)
{
$this->email = $email;
$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 notify(): void
{
foreach ($this->observers as $observer) {
$observer->update($this);
}
}

public function register(): void
{
echo "User: I am registering {$this->email}.\n";
$this->notify();
}

public function getEmail(): string
{
return $this->email;
}
}

class NewsletterSystem implements SplObserver
{
public function update(SplSubject $subject): void
{
if ($subject instanceof User) {
echo "NewsletterSystem: Adding {$subject->getEmail()} to the mailing list.\n";
}
}
}

class Logger implements SplObserver
{
public function update(SplSubject $subject): void
{
if ($subject instanceof User) {
echo "Logger: User registration event for {$subject->getEmail()} logged.\n";
}
}
}

// Клиентский код
$user = new User('[email protected]');

$newsletter = new NewsletterSystem();
$logger = new Logger();

$user->attach($newsletter);
$user->attach($logger);

$user->register();

Что такое паттерн State?​

Middle

Ответ: State — это поведенческий паттерн проектирования, который позволяет объекту изменять своё поведение при изменении его внутреннего состояния. Со стороны кажется, что объект сменил свой класс.

Типичное использование / Плюсы и минусы:

  • Плюсы: Принцип единственной ответственности (SRP). Организуйте код, связанный с конкретными состояниями, в отдельные классы. Принцип открытости/закрытости (OCP). Вводите новые состояния без изменения существующих классов состояний или контекста. Упрощайте код контекста, устраняя громоздкие условные конструкции конечного автомата.
  • Минусы: Применение паттерна может быть избыточным, если конечный автомат имеет лишь несколько состояний или редко изменяется.

Пример:

<?php

// Класс AudioPlayer выступает в роли контекста. Он также
// хранит ссылку на экземпляр одного из классов состояний,
// представляющий текущее состояние аудиоплеера.
class AudioPlayer
{
private State $state;
public bool $playing = false;

public function __construct()
{
$this->state = new ReadyState($this);
}

// Другие объекты должны иметь возможность переключать
// активное состояние аудиоплеера.
public function changeState(State $state): void
{
$this->state = $state;
}

// Методы UI делегируют выполнение активному состоянию.
public function clickLock(): void
{
$this->state->clickLock();
}

public function clickPlay(): void
{
$this->state->clickPlay();
}

public function clickNext(): void
{
$this->state->clickNext();
}

public function clickPrevious(): void
{
$this->state->clickPrevious();
}

// Состояние может вызывать некоторые сервисные методы
// контекста.
public function startPlayback(): void
{
$this->playing = true;
echo "Start playback\n";
}

public function stopPlayback(): void
{
$this->playing = false;
echo "Stop playback\n";
}

public function nextSong(): void
{
echo "Next song\n";
}

public function previousSong(): void
{
echo "Previous song\n";
}

public function fastForward(int $time): void
{
echo "Fast forward {$time}s\n";
}

public function rewind(int $time): void
{
echo "Rewind {$time}s\n";
}
}

// Базовый класс состояния объявляет методы, которые должны
// реализовать все конкретные состояния, а также предоставляет
// обратную ссылку на объект контекста, связанный с состоянием.
// Состояния могут использовать эту обратную ссылку для перевода
// контекста в другое состояние.
abstract class State
{
public function __construct(protected AudioPlayer $player)
{
}

abstract public function clickLock(): void;
abstract public function clickPlay(): void;
abstract public function clickNext(): void;
abstract public function clickPrevious(): void;
}

// Конкретные состояния реализуют различные варианты поведения,
// связанные с состоянием контекста.
class LockedState extends State
{
// При разблокировке заблокированного плеера он может
// принять одно из двух состояний.
public function clickLock(): void
{
if ($this->player->playing) {
$this->player->changeState(new PlayingState($this->player));
} else {
$this->player->changeState(new ReadyState($this->player));
}
}

public function clickPlay(): void
{
// Заблокирован, ничего не делаем.
}

public function clickNext(): void
{
// Заблокирован, ничего не делаем.
}

public function clickPrevious(): void
{
// Заблокирован, ничего не делаем.
}
}

// Они также могут инициировать переходы между состояниями
// в контексте.
class ReadyState extends State
{
public function clickLock(): void
{
$this->player->changeState(new LockedState($this->player));
}

public function clickPlay(): void
{
$this->player->startPlayback();
$this->player->changeState(new PlayingState($this->player));
}

public function clickNext(): void
{
$this->player->nextSong();
}

public function clickPrevious(): void
{
$this->player->previousSong();
}
}

class PlayingState extends State
{
public function clickLock(): void
{
$this->player->changeState(new LockedState($this->player));
}

public function clickPlay(): void
{
$this->player->stopPlayback();
$this->player->changeState(new ReadyState($this->player));
}

public function clickNext(): void
{
$this->player->fastForward(5);
}

public function clickPrevious(): void
{
$this->player->rewind(5);
}
}

$player = new AudioPlayer();
$player->clickPlay();
$player->clickNext();
$player->clickPlay();
$player->clickLock();

Что такое паттерн Strategy?​

Middle

Ответ: Strategy — это поведенческий паттерн проектирования, который позволяет определить семейство алгоритмов, поместить каждый из них в отдельный класс и сделать их объекты взаимозаменяемыми.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете подменять алгоритмы, используемые внутри объекта, во время выполнения. Вы можете изолировать детали реализации алгоритма от кода, который его использует. Вы можете заменить наследование композицией. Принцип открытости/закрытости (OCP). Вы можете вводить новые стратегии без необходимости изменять контекст.
  • Минусы: Если у вас всего пара алгоритмов и они редко меняются, нет реальных причин усложнять программу новыми классами и интерфейсами, которые приходят с паттерном. Клиенты должны знать о различиях между стратегиями, чтобы выбрать подходящую. Многие современные языки программирования поддерживают функциональные типы, что позволяет реализовать различные версии алгоритма в наборе анонимных функций. Тогда вы могли бы использовать эти функции точно так же, как объекты стратегий, но без раздувания кода дополнительными классами и интерфейсами.

Пример:

<?php

// Интерфейс стратегии объявляет операции, общие для всех
// поддерживаемых версий некоторого алгоритма. Контекст
// использует этот интерфейс для вызова алгоритма, определённого
// конкретными стратегиями.
interface Strategy
{
public function execute(int $a, int $b): int;
}

// Конкретные стратегии реализуют алгоритм, следуя базовому
// интерфейсу стратегии. Интерфейс делает их взаимозаменяемыми
// в контексте.
class ConcreteStrategyAdd implements Strategy
{
public function execute(int $a, int $b): int
{
return $a + $b;
}
}

class ConcreteStrategySubtract implements Strategy
{
public function execute(int $a, int $b): int
{
return $a - $b;
}
}

class ConcreteStrategyMultiply implements Strategy
{
public function execute(int $a, int $b): int
{
return $a * $b;
}
}

// Контекст определяет интерфейс, интересующий клиентов.
class Context
{
// Контекст хранит ссылку на один из объектов стратегии.
// Контекст не знает конкретный класс стратегии. Он должен
// работать со всеми стратегиями через интерфейс стратегии.
private Strategy $strategy;

// Обычно контекст принимает стратегию через конструктор, а
// также предоставляет сеттер для того, чтобы стратегию
// можно было сменить во время выполнения.
public function setStrategy(Strategy $strategy): void
{
$this->strategy = $strategy;
}

// Контекст делегирует часть работы объекту стратегии вместо
// того, чтобы самостоятельно реализовывать несколько версий
// алгоритма.
public function executeStrategy(int $a, int $b): int
{
return $this->strategy->execute($a, $b);
}
}

// Клиентский код выбирает конкретную стратегию и передаёт её
// контексту. Клиент должен знать о различиях между
// стратегиями, чтобы сделать правильный выбор.
class ExampleApplication
{
public function main(int $firstNumber, int $secondNumber, string $action): void
{
$context = new Context();

if ($action === 'addition') {
$context->setStrategy(new ConcreteStrategyAdd());
}

if ($action === 'subtraction') {
$context->setStrategy(new ConcreteStrategySubtract());
}

if ($action === 'multiplication') {
$context->setStrategy(new ConcreteStrategyMultiply());
}

$result = $context->executeStrategy($firstNumber, $secondNumber);
echo "Result: {$result}\n";
}
}

$app = new ExampleApplication();
$app->main(10, 5, 'multiplication');

Что такое паттерн Template Method?​

Middle

Ответ: Template Method — это поведенческий паттерн проектирования, который определяет скелет алгоритма в суперклассе, но позволяет подклассам переопределять конкретные шаги алгоритма без изменения его структуры.

Типичное использование / Плюсы и минусы:

  • Плюсы: Вы можете позволить клиентам переопределять только определённые части большого алгоритма, делая их менее подверженными изменениям, затрагивающим другие части алгоритма. Вы можете вынести дублирующийся код в суперкласс.
  • Минусы: Некоторые клиенты могут быть ограничены предоставленным скелетом алгоритма. Вы можете нарушить принцип подстановки Лисков, подавляя реализацию шага по умолчанию через подкласс. Шаблонные методы сложнее поддерживать, чем больше шагов они содержат.

Пример:

<?php

// Абстрактный класс определяет шаблонный метод, содержащий
// скелет некоторого алгоритма, состоящий из вызовов, обычно
// абстрактных примитивных операций. Конкретные подклассы
// реализуют эти операции, но оставляют сам шаблонный метод
// нетронутым.
abstract class GameAI
{
/** @var string[] */
protected array $builtStructures = [];

// Шаблонный метод определяет скелет алгоритма.
final public function turn(): void
{
$this->collectResources();
$this->buildStructures();
$this->buildUnits();
$this->attack();
}

// Некоторые шаги могут быть реализованы прямо в базовом
// классе.
public function collectResources(): void
{
foreach ($this->builtStructures as $structure) {
echo "Collect resources from {$structure}\n";
}
}

// А некоторые могут быть определены как абстрактные.
abstract protected function buildStructures(): void;
abstract protected function buildUnits(): void;

// Класс может иметь несколько шаблонных методов.
protected function attack(): void
{
$enemyPosition = $this->closestEnemy();

if ($enemyPosition === null) {
$this->sendScouts('map.center');
} else {
$this->sendWarriors($enemyPosition);
}
}

protected function closestEnemy(): ?string
{
return null;
}

abstract protected function sendScouts(string $position): void;
abstract protected function sendWarriors(string $position): void;
}

// Конкретные классы должны реализовать все абстрактные
// операции базового класса, но не должны переопределять сам
// шаблонный метод.
class OrcsAI extends GameAI
{
protected function buildStructures(): void
{
echo "Build farms, then barracks, then stronghold.\n";
$this->builtStructures = ['Farm', 'Barracks', 'Stronghold'];
}

protected function buildUnits(): void
{
echo "Build peons and grunts based on available resources.\n";
}

protected function sendScouts(string $position): void
{
echo "Send scouts to {$position}.\n";
}

protected function sendWarriors(string $position): void
{
echo "Send warriors to {$position}.\n";
}
}

// Подклассы также могут переопределять некоторые операции с
// реализацией по умолчанию.
class MonstersAI extends GameAI
{
public function collectResources(): void
{
// Монстры не собирают ресурсы.
}

protected function buildStructures(): void
{
// Монстры не строят здания.
}

protected function buildUnits(): void
{
// Монстры не создают юнитов.
}

protected function sendScouts(string $position): void
{
echo "Monsters roam to {$position}.\n";
}

protected function sendWarriors(string $position): void
{
echo "Monsters attack {$position}.\n";
}
}

$orcs = new OrcsAI();
$orcs->turn();

Что такое паттерн Visitor?​

Middle

Ответ: Visitor — это поведенческий паттерн проектирования, который позволяет отделить алгоритмы от объектов, над которыми они работают.

Типичное использование / Плюсы и минусы:

  • Плюсы: Принцип открытости/закрытости (OCP). Вы можете ввести новое поведение, способное работать с объектами различных классов, не изменяя эти классы. Принцип единственной ответственности (SRP). Вы можете переместить несколько версий одного и того же поведения в один класс. Объект посетителя может накапливать полезную информацию при работе с различными объектами. Это может быть полезно, когда вы хотите обойти некоторую сложную структуру объектов, такую как дерево объектов, и применить посетителя к каждому объекту этой структуры.
  • Минусы: Вам нужно обновлять всех посетителей каждый раз, когда класс добавляется в иерархию элементов или удаляется из неё. Посетители могут не иметь необходимого доступа к приватным полям и методам элементов, с которыми они должны работать.

Пример:

<?php

// Интерфейс элемента объявляет метод `accept`, принимающий
// базовый интерфейс посетителя в качестве аргумента.
interface Shape
{
public function move(int $x, int $y): void;
public function draw(): void;
public function accept(Visitor $visitor): string;
}

// Каждый конкретный класс элемента должен реализовать метод
// `accept` таким образом, чтобы он вызывал метод посетителя,
// соответствующий классу элемента.
class Dot implements Shape
{
public function __construct(protected int $x, protected int $y, protected int $id)
{
}

public function move(int $x, int $y): void
{
$this->x += $x;
$this->y += $y;
}

public function draw(): void
{
echo "Draw dot #{$this->id}\n";
}

public function getId(): int
{
return $this->id;
}

public function getX(): int
{
return $this->x;
}

public function getY(): int
{
return $this->y;
}

// Обратите внимание, что мы вызываем `visitDot`, что
// соответствует имени текущего класса. Таким образом мы
// сообщаем посетителю класс элемента, с которым он
// работает.
public function accept(Visitor $visitor): string
{
return $visitor->visitDot($this);
}
}

class Circle extends Dot
{
public function __construct(int $x, int $y, int $id, private int $radius)
{
parent::__construct($x, $y, $id);
}

public function getRadius(): int
{
return $this->radius;
}

public function accept(Visitor $visitor): string
{
return $visitor->visitCircle($this);
}
}

class Rectangle implements Shape
{
public function __construct(
private int $id,
private int $x,
private int $y,
private int $width,
private int $height
) {
}

public function move(int $x, int $y): void
{
$this->x += $x;
$this->y += $y;
}

public function draw(): void
{
echo "Draw rectangle #{$this->id}\n";
}

public function accept(Visitor $visitor): string
{
return $visitor->visitRectangle($this);
}

public function getId(): int
{
return $this->id;
}

public function getX(): int
{
return $this->x;
}

public function getY(): int
{
return $this->y;
}

public function getWidth(): int
{
return $this->width;
}

public function getHeight(): int
{
return $this->height;
}
}

class CompoundShape implements Shape
{
/** @param Shape[] $children */
public function __construct(private int $id, private array $children = [])
{
}

public function add(Shape $shape): void
{
$this->children[] = $shape;
}

/** @return Shape[] */
public function getChildren(): array
{
return $this->children;
}

public function getId(): int
{
return $this->id;
}

public function move(int $x, int $y): void
{
foreach ($this->children as $child) {
$child->move($x, $y);
}
}

public function draw(): void
{
foreach ($this->children as $child) {
$child->draw();
}
}

public function accept(Visitor $visitor): string
{
return $visitor->visitCompoundShape($this);
}
}

// Интерфейс Visitor объявляет набор методов посещения,
// соответствующих классам элементов. Сигнатура метода
// посещения позволяет посетителю определить точный класс
// элемента, с которым он имеет дело.
interface Visitor
{
public function visitDot(Dot $dot): string;
public function visitCircle(Circle $circle): string;
public function visitRectangle(Rectangle $rectangle): string;
public function visitCompoundShape(CompoundShape $compoundShape): string;
}

// Конкретные посетители реализуют несколько версий одного и
// того же алгоритма, способного работать со всеми конкретными
// классами элементов.
//
// Наибольшую выгоду от паттерна Visitor вы получите при
// использовании его со сложной структурой объектов, такой как
// дерево Composite. В этом случае может быть полезно хранить
// некоторое промежуточное состояние алгоритма при выполнении
// методов посетителя над различными объектами структуры.
class XMLExportVisitor implements Visitor
{
public function visitDot(Dot $dot): string
{
return sprintf('<dot id="%d" x="%d" y="%d"/>', $dot->getId(), $dot->getX(), $dot->getY());
}

public function visitCircle(Circle $circle): string
{
return sprintf(
'<circle id="%d" x="%d" y="%d" radius="%d"/>',
$circle->getId(),
$circle->getX(),
$circle->getY(),
$circle->getRadius()
);
}

public function visitRectangle(Rectangle $rectangle): string
{
return sprintf(
'<rectangle id="%d" x="%d" y="%d" width="%d" height="%d"/>',
$rectangle->getId(),
$rectangle->getX(),
$rectangle->getY(),
$rectangle->getWidth(),
$rectangle->getHeight()
);
}

public function visitCompoundShape(CompoundShape $compoundShape): string
{
$childrenXml = [];
foreach ($compoundShape->getChildren() as $child) {
$childrenXml[] = $child->accept($this);
}

return sprintf('<compound id="%d">%s</compound>', $compoundShape->getId(), implode('', $childrenXml));
}
}

// Клиентский код может выполнять операции посетителя над любым
// набором элементов, не выясняя их конкретные классы. Операция
// accept направляет вызов к соответствующей операции в объекте
// посетителя.
class Application
{
/** @var Shape[] */
private array $allShapes;

public function __construct()
{
$compound = new CompoundShape(10);
$compound->add(new Dot(1, 2, 1));
$compound->add(new Circle(5, 3, 2, 10));

$this->allShapes = [
new Dot(10, 20, 3),
new Rectangle(4, 0, 0, 50, 100),
$compound,
];
}

public function export(): void
{
$exportVisitor = new XMLExportVisitor();

foreach ($this->allShapes as $shape) {
echo $shape->accept($exportVisitor) . "\n";
}
}
}

$app = new Application();
$app->export();

Что такое Dependency Injection?​

Senior

Ответ: Паттерн проектирования, при котором объект получает свои зависимости извне (например, через конструктор), а не создаёт их самостоятельно. Это способствует слабой связанности (loose coupling) и упрощает тестирование.


В чём разница между Dependency Injection и Dependency Inversion?​

Senior

Ответ:

  • Dependency Injection (DI): Это паттерн передачи зависимостей объекту извне.
  • Dependency Inversion (DIP): Это принцип (буква D в SOLID), согласно которому модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций (интерфейсов). DI является основным инструментом реализации DIP.

Как принцип подстановки Лисков (LSP) реализуется через типизацию в PHP?​

Senior

Ответ:

  • Ковариантность (Covariance): Позволяет дочернему методу возвращать более специфичный тип (поддерживается с PHP 7.4).
  • Контрвариантность (Contravariance): Позволяет дочернему методу принимать более широкий тип аргумента (поддерживается с PHP 7.4). Согласно LSP, дочерний класс не должен усиливать предусловия (контрвариантность) и не должен ослаблять постусловия (ковариантность). Подробный разбор SOLID

В чём разница между паттерном Factory и Dependency Injection?​

Senior

Ответ: Паттерн Factory отвечает за создание объектов, тогда как Dependency Injection — за предоставление уже созданных зависимостей объекту.


Какие основные паттерны проектирования используются в Symfony и Doctrine?​

Senior

Ответ: