Шаблоны проектирования, которые распределяют обязанности между объектами и управляют их взаимодействием. Они делают коммуникацию между элементами системы более гибкой и эффективной.

Стратегия

Который определяет семейство схожих алгоритмов, выносит каждый из них в отдельный класс и делает их взаимозаменяемыми. Это позволяет менять логику программы прямо во время работы (в runtime), не меняя код классов, которые эти алгоритмы используют. Очень часто встречаемый паттерн, приведу характерный пример который у меня был в компании. В нашем интернет магазине (ИМ) были интеграции с различными программами лояльности, был сделан общий интерфейс. После перехода с одной программы лояльности на другую, нам необходимо было обрабатывать заказы разными способами, для ранее созданных использовать старую программу лояльности (ПЛ), вот для этого и предназначены стратегии. Где то мы смотрели флаг ПЛ и обрабатывали по старому, а все новые заказы шли уже по новому.

<?php

// Интерфейс для программы лояльности
interface LoyaltyStrategyInterface 
{
    /**
     * Рассчитывает итоговую сумму к оплате с учетом скидки
     */
    public function calculateDiscount(float $orderAmount, int $userId): float;

    /**
     * Фиксирует покупку в системе лояльности (например, начисляет кэшбэк)
     */
    public function processOrder(float $finalAmount, int $userId, string $orderId): void;
}

Теперь перейдем к конкретным стратегиям:

// Локальная стратегия
class LocalLoyaltyStrategy implements LoyaltyStrategyInterface 
{
    public function calculateDiscount(float $orderAmount, int $userId): float 
    {
        // Бизнес-логика: ищем пользователя в БД, смотрим его уровень (например, Gold = 10%)
        $discountPercent = 0.10; 
        return $orderAmount * $discountPercent;
    }

    public function processOrder(float $finalAmount, int $userId, string $orderId): void 
    {
        // Начисляем 5% внутренними бонусами на баланс в нашей БД
        $bonusAmount = $finalAmount * 0.05;
        echo "Заказ {$orderId}: Начислено {$bonusAmount} внутренних бонусов пользователю {$userId}.\n";
    }
}

// Mindbox стратегия
class MindboxLoyaltyStrategy implements LoyaltyStrategyInterface 
{
    private $httpClient;

    public function __construct($httpClient) // Передаем HTTP-клиент (Guzzle/Curl)
    {
        $this->httpClient = $httpClient;
    }

    public function calculateDiscount(float $orderAmount, int $userId): float 
    {
        echo "Запрос к API Mindbox: расчет персональных привилегий для юзера {$userId}...\n";
        
        // В реальности здесь отправляется POST-запрос к Mindbox API (например, /v3/operations/sync)
        // С передачей корзины и ID пользователя. Mindbox возвращает доступную скидку.
        $mindboxApiResponse = ['discount_value' => 350.00]; 

        return $mindboxApiResponse['discount_value'];
    }

    public function processOrder(float $finalAmount, int $userId, string $orderId): void 
    {
        echo "Запрос к API Mindbox: регистрация заказа {$orderId}...\n";
        // Отправка транзакции в Mindbox для списания/начисления баллов (операция /v3/operations/async)
    }
}

И уже в коде, мы можем сделать маппинг типа ПЛ с стратегией и обрабатывать заказ нужной стратегией.

Состояние

Паттерн который позволяет объекту изменять свое поведение в зависимости от его внутреннего состояния. Изнутри это выглядит так, будто объект полностью сменил свой класс.

Главное отличие от паттерна Стратегия

  • Стратегия меняет алгоритм выполнения задачи. Клиент обычно сам знает, какую стратегию выбрать, и они редко сменяют друг друга на лету.
  • Состояние меняет всё поведение объекта. Состояния сами переводят контекст из одного режима в другой по цепочке (например, из «Черновика» в «На модерации»).

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

// Интерфейс для состояний отзыва
interface ReviewState {
    public function sendToModerate(ReviewContext $context): void;
    public function approve(ReviewContext $context): void;
    public function reject(ReviewContext $context): void;
}

// Сам отзыв
class Review {
    private ReviewState $state;
    private string $text;

    public function __construct(string $text) {
        $this->text = $text;
        // По умолчанию новый отзыв создается как черновик
        $this->state = new DraftState();
    }

    public function setState(ReviewState $state): void {
        $this->state = $state;
    }

    public function getText(): string {
        return $this->text;
    }

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

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

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

Описание основных состояний с поведениями:

class DraftState implements ReviewState {
    public function sendToModerate(ReviewContext $context): void {
        echo "Отзыв отправлен на модерацию.\n";
        $context->setState(new ModerationState());
    }

    public function approve(ReviewContext $context): void {
        echo "Ошибка: нельзя опубликовать черновик в обход модерации.\n";
    }

    public function reject(ReviewContext $context): void {
        echo "Ошибка: нельзя отклонить черновик, он еще не на проверке.\n";
    }
}

// 4. Состояние: На модерации
class ModerationState implements ReviewState {
    public function sendToModerate(ReviewContext $context): void {
        echo "Ошибка: отзыв уже проверяется модератором.\n";
    }

    public function approve(ReviewContext $context): void {
        echo "Отзыв успешно одобрен и опубликован на сайте!\n";
        $context->setState(new PublishedState());
    }

    public function reject(ReviewContext $context): void {
        echo "Отзыв отклонен модератором и возвращен в черновики для исправления.\n";
        $context->setState(new DraftState());
    }
}

// 5. Состояние: Опубликован
class PublishedState implements ReviewState {
    public function sendToModerate(ReviewContext $context): void {
        echo "Ошибка: опубликованный отзыв нельзя повторно модерировать.\n";
    }

    public function approve(ReviewContext $context): void {
        echo "Предупреждение: отзыв уже был опубликован ранее.\n";
    }

    public function reject(ReviewContext $context): void {
        echo "Отзыв скрыт с сайта и отправлен обратно на доработку.\n";
        $context->setState(new DraftState());
    }
}

Наблюдатель

Паттерн который создает механизм подписки. Он позволяет одним объектам (наблюдателям) следить за изменениями в другом объекте (субъекте) и автоматически реагировать на них. Очень распространенный паттерн, реализуется например в качестве создания подписчиков на события, как правило в связки с паттерном состояние. Пример с заказами, изменение состояния заказа на оплачен, мы инициируем событие изменения статуса заказа, на это событие подписаны сразу несколько подписчиков, рассылка уведомлений, формирование чека и т.д., все они не зависимы друг от друга.

<?php

namespace App\Message;

// Делаем событие оплаты заказа
class OrderPaidMessage
{
    // Передаем только ID, чтобы хендлеры брали актуальные данные из БД
    public function __construct(private int $orderId) {}

    public function getOrderId(): int
    {
        return $this->orderId;
    }
}

// Регистрируем подписчика на отправку уведомления
#[AsMessageHandler]
class SmsNotificationHandler
{
    public function __invoke(OrderPaidMessage $message): void
    {
        // Логика интеграции с SMS-шлюзом
        echo "SMS-Центр: Отправлено уведомление клиенту: 'Ваш заказ #{$message->getOrderId()} успешно оплачен!'\n";
    }
}

// Регистрируем событие на генерацию чека 
#[AsMessageHandler]
class FiscalReceiptHandler
{
    public function __construct(private OrderRepository $orderRepository) {}

    public function __invoke(OrderPaidMessage $message): void
    {
        $order = $this->orderRepository->find($message->getOrderId());
        if (!$order) return;

        // Тяжелый запрос к API онлайн-кассы (Атол, Эвотор и т.д.)
        echo "Чекопечаталка: Сгенерирован фискальный чек для заказа #{$order->getId()} на сумму {$order->getAmount()} руб.\n";
    }
}

Шаблонный метод

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

В моей практике с помощью данного шаблона реализовал генерацию отчетов, сделал абстрактный класс в котором описал стандартные шаги генерации отчета и уже определил реализации для конкретных типов отчетов

<?php

// 1. Абстрактный базовый класс отчета (Шаблон)
abstract class ReportGenerator
{
    // Финальный шаблонный метод. Задает жесткий порядок шагов.
    final public function generateReport(int $month): string
    {
        $rawData = $this->fetchData($month);
        $preparedData = $this->prepareData($rawData);
        
        $formattedContent = $this->formatData($preparedData);
        $filePath = $this->saveToFile($formattedContent);
        
        $this->sendNotification($filePath); // Хук
        
        return $filePath;
    }

    // Общий шаг: Чтение данных (одинаково для всех отчетов, например из БД)
    private function fetchData(int $month): array
    {
        echo "База данных: Загрузка сырых данных за {$month}-й месяц...\n";
        return [
            ['id' => 1, 'user' => 'Иван', 'amount' => 1500],
            ['id' => 2, 'user' => 'Ольга', 'amount' => 3200],
            ['id' => 3, 'user' => 'Пётр', 'amount' => 800],
        ];
    }

    // Общий шаг: Предподготовка (очистка, сортировка, фильтрация данных)
    private function prepareData(array $data): array
    {
        echo "Препроцессинг: Фильтрация и подсчет итоговых сумм...\n";
        $total = 0;
        foreach ($data as $item) {
            $total += $item['amount'];
        }
        return [
            'items' => $data,
            'total_amount' => $total,
            'generated_at' => date('Y-m-d H:i:s')
        ];
    }

    // Абстрактный шаг: Форматирование (каждый отчет делает по-своему)
    abstract protected function formatData(array $preparedData): string;

    // Абстрактный шаг: Запись в файл (у каждого формата свое расширение и логика записи)
    abstract protected function saveToFile(string $content): string;

    // Необязательный шаг (Хук): По умолчанию ничего не делает, но подклассы могут использовать
    protected function sendNotification(string $filePath): void
    {
        // По умолчанию пусто
    }
}

И задал отдельные классы для разных отчетов

// Генерация отчетов в формате CSV
class CsvReportGenerator extends ReportGenerator
{
    protected function formatData(array $preparedData): string
    {
        echo "Форматирование: Сборка CSV строки...\n";
        $csv = "ID,Пользователь,Сумма\n";
        foreach ($preparedData['items'] as $item) {
            $csv .= "{$item['id']},{$item['user']},{$item['amount']}\n";
        }
        $csv .= "Итого,,{$preparedData['total_amount']}\n";
        return $csv;
    }

    protected function saveToFile(string $content): string
    {
        $filename = "report_" . time() . ".csv";
        echo "Файловая система: Запись данных в файл {$filename}\n";
        // Имитация file_put_contents($filename, $content);
        return "/storage/reports/" . $filename;
    }
}

// Генерация отчета в формате HTML
class HtmlReportGenerator extends ReportGenerator
{
    protected function formatData(array $preparedData): string
    {
        echo "Форматирование: Рендеринг HTML шаблона...\n";
        $html = "<h1>Финансовый отчет</h1><ul>";
        foreach ($preparedData['items'] as $item) {
            $html .= "<li>{$item['user']}: {$item['amount']} руб.</li>";
        }
        $html .= "</ul><p><b>Всего: {$preparedData['total_amount']} руб.</b></p>";
        return $html;
    }

    protected function saveToFile(string $content): string
    {
        $filename = "report_" . time() . ".html";
        echo "Файловая система: Генерация HTML-страницы {$filename}\n";
        return "/storage/reports/" . $filename;
    }

    // Переопределяем хук, так как HTML/PDF отчеты критически важны
    protected function sendNotification(string $filePath): void
    {
        echo "Уведомление: Ссылка на HTML-отчет отправлена в Slack-канал бухгалтерии ({$filePath}).\n";
    }
}

Посетитель

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

<?php

// 1. Единый интерфейс элемента
interface Element {
    public function accept(Visitor $visitor): float;
}

// 2. Универсальный интерфейс посетителя
interface Visitor {
    public function visit(Element $element): float;
}

Добавляем классы элементов

class Food implements Element {
    public function __construct(private float $price) {}
    public function getPrice(): float { return $this->price; }
    
    public function accept(Visitor $visitor): float {
        return $visitor->visit($this); // Просто передаем себя
    }
}

class Electronics implements Element {
    public function __construct(private float $price) {}
    public function getPrice(): float { return $this->price; }
    
    public function accept(Visitor $visitor): float {
        return $visitor->visit($this);
    }
}

class Clothing implements Element {
    public function __construct(private float $price) {}
    public function getPrice(): float { return $this->price; }
    
    public function accept(Visitor $visitor): float {
        return $visitor->visit($this);
    }
}

Теперь делаем класс посетителя

class RussiaTaxVisitor implements Visitor {
    public function visit(Element $element): float {
        return match (true) {
            $element instanceof Food => $element->getPrice() * 0.10,        // НДС 10%
            $element instanceof Electronics => $element->getPrice() * 0.20, // НДС 20%
            
            // Если посетитель "не знает" про новый тип товара (например, Одежду),
            // мы можем безопасно вернуть дефолтное значение или налог по умолчанию.
            $element instanceof Clothing => $element->getPrice() * 0.20,    // Налог на одежду 20%
            
            default => 0.0
        };
    }
}