Шаблоны проектирования, которые распределяют обязанности между объектами и управляют их взаимодействием. Они делают коммуникацию между элементами системы более гибкой и эффективной.
Стратегия
Который определяет семейство схожих алгоритмов, выносит каждый из них в отдельный класс и делает их взаимозаменяемыми. Это позволяет менять логику программы прямо во время работы (в 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
};
}
}
0 Сообщений