Структурные паттерны это шаблоны которые определяют структуру компонентов и их взаимосвязь.
Адаптер
Это шаблон который позволяет связать между собой два объекта с разными интерфейсами. В моей практике очень часто встречался данный паттерн, приведу реальный пример, есть интернет магазин, у него куча интеграций с разными API служб доставки, у каждой службы доставки есть свои нюансы, СДК и т.д., для того что бы с ними было удобно работать в коде, мы в логике используем один общий интерфейс для них, но вот не задача, интерфейсы у всех доставок разные и вот что бы решить эту проблему делают адаптер.
<?php
// Единый интерфейс доставки, который непосредственно учавствует в бизнес логике
interface DeliveryServiceInterface {
public function calculateCost(int $weightInGrams, string $zipCode): float;
public function createShipment(string $address, array $items): string;
}
// Сторонняя библиотека СДЭК (CdekApiClient)
class CdekSdk {
public function getTariff(string $postalCode, float $weightInKg): float {
// Логика запроса к API СДЭК...
return 450.00;
}
public function registerOrder(array $deliveryData): array {
// Логика создания заказа в СДЭК
// Ожидает структуру: ['delivery_address' => '...', 'cargo' => [...]]
return ['cdek_id' => 'CDEK-998877'];
}
}
// Адаптер, который реализует общий интерфейс
class CdekAdapter implements DeliveryServiceInterface {
private CdekSdk $cdekApi;
public function __construct(CdekSdk $cdekApi) {
$this->cdekApi = $cdekApi;
}
// Адаптируем расчет стоимости
public function calculateCost(int $weightInGrams, string $zipCode): float {
// Конвертируем граммы в килограммы для СДЭК
$weightInKg = $weightInGrams / 1000;
// Вызываем метод из SDK СДЭК
return $this->cdekApi->getTariff($zipCode, $weightInKg);
}
// Адаптируем создание отправления
public function createShipment(string $address, array $items): string {
// Пересобираем массив под формат, который требует СДЭК
$cdekFormatData = [
'delivery_address' => $address,
'cargo' => $items
];
$response = $this->cdekApi->registerOrder($cdekFormatData);
// Возвращаем только трек-номер в едином строковом формате нашего сайта
return $response['cdek_id'];
}
}
Таким образом, мы адаптируем стороннюю библиотеку, для работы с нашей бизнес логикой.
Мост
Этот паттерн немного схож с Адаптером, но он не просто делает адаптирование одного интерфейса к другом, а как бы проектирует структуру для возможных расширения функционала, путем деления его на абстракцию и реализацию. Самый простой пример который у меня был на практике, это различные способы отправки СМС, по мере жизни интернет магазина, менялись провайдеры для отправки СМС клиентам, при проектирование мы сразу заложили, что бизнес в любой момент может сменить на другого провайдера который будет дешевле или более подходящим.
<?php
// Интерфейс реализации
interface MessageGateway {
public function connect(): void;
public function transmit(string $to, string $body): bool;
}
// Провайдер 1: Сервис StreamTelekom
class StreamTelekomGateway implements MessageGateway {
public function connect(): void { /* Логика авторизации в API StreamTelekom */ }
public function transmit(string $to, string $body): bool {
echo "[StreamTelekom API] Отправлено на {$to}: \"{$body}\"\n";
return true;
}
}
// Провайдер 2: Сервис Beeline
class BeelineGateway implements MessageGateway {
public function connect(): void { /* Логика авторизации в API Beeline */ }
public function transmit(string $to, string $body): bool {
echo "[Beeline API] Отправлено на {$to}: \"{$body}\"\n";
return true;
}
}
Тогда абстракция будет выглядеть так:
<?php
// Абстракция
abstract class Notification {
// Тот самый Мост — ссылка на интерфейс реализации
protected MessageGateway $gateway;
public function __construct(MessageGateway $gateway) {
$this->gateway = $gateway;
}
abstract public function send(string $recipient, string $message): void;
}
// Расширенная абстракция: Обычное текстовое уведомление
class PlainNotification extends Notification {
public function send(string $recipient, string $message): void {
$this->gateway->connect();
// Отправляем текст как есть
$this->gateway->transmit($recipient, $message);
}
}
Теперь при появлении новых сервисов по отправке мы можем просто добавлять новый провайдер, не меняя старый код.
Компоновщик
Это паттерн который позволяет объединять объекты в древовидные структуры и работать с ними так, будто это один единственный объект. Наглядный пример это реализация дерева категорий товаров в интернет магазине. У нас товары, есть категории, категория может содержать как товары так и подкатегории.
<?php
// Интерфейс для элемента каталога
interface CatalogComponent {
public function getProductsCount(): int;
}
// Товар
class Product implements CatalogComponent {
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
// Товар всегда считает себя за единицу
public function getProductsCount(): int {
return 1;
}
}
// Категория
class Category implements CatalogComponent {
private string $name;
private array $children = []; // Здесь могут быть и Product, и Category
public function __construct(string $name) {
$this->name = $name;
}
// Метод для добавления элементов в коробку/категорию
public function add(CatalogComponent $component): void {
$this->children[] = $component;
}
// Самая важная часть: контейнер делегирует работу своим детям
public function getProductsCount(): int {
$total = 0;
foreach ($this->children as $child) {
// Рекурсивно вызываем тот же метод у детей
$total += $child->getProductsCount();
}
return $total;
}
}
Точно так же можно привести пример с заказом и позициями в заказе, допустим когда хотим посчитать вес\стоимость заказа.
Декоратор
Паттерн, который позволяет динамически добавлять объектам новые обязанности на лету. При этом вы не меняете исходный код самого объекта и не используете громоздкое наследование. На практике, частенько происходит что используя какой-то класс, нам не хватает какой-то мелочи в его реализации, не обязательно даже мелочи, писать новый класс выглядит нагружено, да и не во всех случаях возможно нам это нужно, тогда можно прибегнуть к декорирования или обертыванию класса и последующем его дополнением. Допустим у нас была отправка сообщения на почту, в какой-то момент нам потребовалось логировать часть отправляемых сообщений, а часть оставить по прежнему.
<?php
// Интерфейс для отправки сообщения
interface Notifier {
public function send(string $message): void;
}
// Класс который отправляет сообщение на почту
class EmailNotifier implements Notifier {
public function send(string $message): void {
echo "📧 Отправлено Email-письмо с текстом: \"{$message}\"\n";
}
}
Для того, что бы добавить логирование, мы пишем декоратор
<?php
// Абстракция для декоратора
abstract class NotifierDecorator implements Notifier {
protected Notifier $wrappedNotifier;
public function __construct(Notifier $notifier) {
$this->wrappedNotifier = $notifier;
}
// По умолчанию просто перенаправляет вызов обернутому объекту
public function send(string $message): void {
$this->wrappedNotifier->send($message);
}
}
// Декоратор: добавляет логирование
class LoggingDecorator extends NotifierDecorator {
public function send(string $message): void {
// Свое действие ДО отправки
echo "📝 [LOG] Попытка отправки сообщения в " . date('H:i:s') . "\n";
parent::send($message);
}
}
Фасад
Паттерн, который предоставляет один простой интерфейс для работы с сложной системой классов или методов. Представим, что в нашем интернет магазине при создании заказа, нужно забронировать товар на складе, посчитать бонусы, выставить счет, отправить клиенту уведомления, без фасада контроллеру пришлось бы работать с кучей классов.
<?php
class InventorySystem {
public function isAvailable(string $productId): bool { return true; }
public function reserve(string $productId): void { echo "📦 Товар зарезервирован на складе.\n"; }
}
class PaymentGateway {
public function charge(float $amount): bool {
echo "💳 Списано {$amount} руб. через эквайринг.\n";
return true;
}
}
class InvoiceGenerator {
public function createReceipt(): void { echo "🧾 Чек сформирован и отправлен в налоговую.\n"; }
}
class SmsNotifier {
public function sendNotification(): void { echo "📱 SMS: 'Ваш заказ успешно оформлен!'\n"; }
}
Делаем фасад, который прячет всю эту рутину внутри себя. Он знает, в каком порядке вызывать методы подсистемы.
<?php
class OrderFacade {
private InventorySystem $inventory;
private PaymentGateway $payment;
private InvoiceGenerator $invoice;
private SmsNotifier $sms;
// Внедряем все сложные зависимости в конструктор
public function __construct(
InventorySystem $inventory,
PaymentGateway $payment,
InvoiceGenerator $invoice,
SmsNotifier $sms
) {
$this->inventory = $inventory;
$this->payment = $payment;
$this->invoice = $invoice;
$this->sms = $sms;
}
// Единственный простой метод для внешнего мира
public function placeOrder(string $productId, float $price): bool {
echo "=== НАЧАЛО ОФОРМЛЕНИЯ ЗАКАЗА ===\n";
if (!$this->inventory->isAvailable($productId)) {
echo "❌ Товара нет на складе.\n";
return false;
}
$this->inventory->reserve($productId);
$this->payment->charge($price);
$this->invoice->createReceipt();
$this->sms->sendNotification();
echo "=== ЗАКАЗ УСПЕШНО ЗАВЕРШЕН ===\n";
return true;
}
}
Приспособленец
Это паттерн который позволяет экономить память на общих данных. Допустим у нас есть класс товара, который содержит просто куча общей информации с целой группой товаров, зачем нам занимать память всеми этими данными для тысячи товаров, когда можно их вынести в отдельный класс приспособленец.
<?php
// Приспособленец, хранящий общие свойства
class ProductMetadata {
private string $brand;
private string $category;
private array $warrantyDetails; // Тяжелый массив с правилами возврата и гарантии
public function __construct(string $brand, string $category, array $warrantyDetails) {
$this->brand = $brand;
$this->category = $category;
$this->warrantyDetails = $warrantyDetails;
}
// Метод принимает внешнее (уникальное) состояние товара извне в момент рендера
public function renderCard(string $sku, float $price, int $stock): void {
echo "🛒 Товар [SKU: {$sku}] | Бренд: {$this->brand} | Категория: {$this->category} | ";
echo "Цена: {$price} руб. | Остаток: {$stock} шт.\n";
}
}
// Продукт
class ProductContext {
private string $sku;
private float $price;
private int $stock;
// Ссылка на приспособленца
private ProductMetadata $metadata;
public function __construct(string $sku, float $price, int $stock, ProductMetadata $metadata) {
$this->sku = $sku;
$this->price = $price;
$this->stock = $stock;
$this->metadata = $metadata;
}
public function display(): void {
// Передаем уникальное состояние внутрь приспособленца для вывода
$this->metadata->renderCard($this->sku, $this->price, $this->stock);
}
}
Заместитель
Заместитель очень похож по своей реализации на декоратор, но несет в себе другую цель. Декоратор используется для расширения функционала, а заместитель, как бы перехватывает вызовы. Самый частый и простой пример, это кеширование запросов в какой-нибудь сервис, API. Что бы каждый раз не грузить данные которые редко меняются, мы можем сделать обертку, в которой проверять наличие закешированного результат и отдавать сразу готовый результат.
<?php
// Интерфейс для генерации отчета
interface ReportGenerator {
public function generateMonthlyReport(string $month): string;
}
// Класс реальной генерации отчета
class RealReportGenerator implements ReportGenerator {
public function generateMonthlyReport(string $month): string {
// Эмуляция тяжелого запроса к внешнему API / БД
echo "⏳ [API] Запрашиваем терабайты данных за {$month}... (это заняло 5 секунд)\n";
sleep(5);
return "📊 Финансовый отчет за {$month}: Выручка +150%, Расходы -20%";
}
}
class ReportCache implements ReportGenerator {
private RealReportGenerator $realService;
private array $cache = [];
private string $userRole;
public function __construct(RealReportGenerator $realService, string $userRole) {
$this->realService = $realService;
$this->userRole = $userRole; // Передаем роль текущего пользователя
}
public function generateMonthlyReport(string $month): string {
// 1. Проверяющий прокси (Access Control)
if ($this->userRole !== 'ADMIN' && $this->userRole !== 'CEO') {
throw new Exception("🚫 Ошибка доступа: У вас нет прав для просмотра финансовых отчетов!");
}
// 2. Кэширующий прокси (Caching)
if (isset($this->cache[$month])) {
echo "⚡ [PROXY] Возвращаем отчет за {$month} из КЭША мгновенно!\n";
return $this->cache[$month];
}
// 3. Если в кэше нет и права есть — делегируем работу реальному сервису
echo "🔍 [PROXY] В кэше ничего нет. Обращаемся к реальному сервису...\n";
$report = $this->realService->generateMonthlyReport($month);
// Сохраняем в кэш на будущее
$this->cache[$month] = $report;
return $report;
}
}
0 Сообщений