🧩 Структурные и порождающие паттерны

Decorator, Adapter, Builder

Три мощных паттерна проектирования для гибкой и расширяемой архитектуры. Разбираем с примерами на PHP.

🎀 Decorator 🔌 Adapter 🏗️ Builder

🎀 Decorator — декоратор

🎀

Decorator

Структурный паттерн
Decorator позволяет динамически добавлять объектам новую функциональность, оборачивая их в полезные «обёртки», не изменяя исходный код класса.

🎯 Когда использовать

Нужно добавить поведение без изменения класса
Комбинировать поведения в любом порядке
Наследование не подходит или невозможно
Добавлять/убирать функционал в рантайме

❌ Проблема без Decorator

Представьте, что у вас есть система уведомлений. Изначально она отправляет только email. Но потом нужно добавить SMS, Slack, Telegram... И комбинации всего этого!

PHP Без паттерна — взрыв классов
// Базовый класс
class EmailNotifier { }

// Нужен email + SMS? Создаём новый класс
class EmailSmsNotifier extends EmailNotifier { }

// Email + Slack?
class EmailSlackNotifier extends EmailNotifier { }

// Email + SMS + Slack?
class EmailSmsSlackNotifier extends EmailSmsNotifier { }

// Email + SMS + Slack + Telegram?
class EmailSmsSlackTelegramNotifier extends EmailSmsSlackNotifier { }

// 💥 При 5 каналах = 2^5 = 32 класса!
// 💥 При 10 каналах = 1024 класса!

✅ Решение с Decorator

Структура паттерна Decorator
Компонент
Декоратор 1
Декоратор 2
Декоратор N

Примеры кода

PHP Decorator Pattern
// Интерфейс компонента
interface NotifierInterface
{
    public function send(string $message): void;
}

// Базовая реализация
class EmailNotifier implements NotifierInterface
{
    public function __construct(
        private string $email,
    ) {}

    public function send(string $message): void
    {
        echo "📧 Email to {$this->email}: {$message}\n";
    }
}

// Базовый декоратор
abstract class NotifierDecorator implements NotifierInterface
{
    public function __construct(
        protected NotifierInterface $wrappee,
    ) {}

    public function send(string $message): void
    {
        $this->wrappee->send($message);
    }
}

// Декоратор SMS
class SmsNotifierDecorator extends NotifierDecorator
{
    public function __construct(
        NotifierInterface $wrappee,
        private string $phone,
    ) {
        parent::__construct($wrappee);
    }

    public function send(string $message): void
    {
        parent::send($message);
        echo "📱 SMS to {$this->phone}: {$message}\n";
    }
}

// Декоратор Slack
class SlackNotifierDecorator extends NotifierDecorator
{
    public function __construct(
        NotifierInterface $wrappee,
        private string $channel,
    ) {
        parent::__construct($wrappee);
    }

    public function send(string $message): void
    {
        parent::send($message);
        echo "💬 Slack to #{$this->channel}: {$message}\n";
    }
}

// Декоратор Telegram
class TelegramNotifierDecorator extends NotifierDecorator
{
    public function __construct(
        NotifierInterface $wrappee,
        private string $chatId,
    ) {
        parent::__construct($wrappee);
    }

    public function send(string $message): void
    {
        parent::send($message);
        echo "✈️ Telegram to {$this->chatId}: {$message}\n";
    }
}
PHP Использование
// Только email
$notifier = new EmailNotifier('user@test.com');
$notifier->send('Hello!');
// 📧 Email to user@test.com: Hello!

// Email + SMS
$notifier = new SmsNotifierDecorator(
    new EmailNotifier('user@test.com'),
    '+7-999-123-45-67'
);
$notifier->send('Hello!');
// 📧 Email to user@test.com: Hello!
// 📱 SMS to +7-999-123-45-67: Hello!

// Email + SMS + Slack + Telegram — любая комбинация!
$notifier = new TelegramNotifierDecorator(
    new SlackNotifierDecorator(
        new SmsNotifierDecorator(
            new EmailNotifier('user@test.com'),
            '+7-999-123-45-67'
        ),
        'alerts'
    ),
    '123456789'
);

$notifier->send('Server is down!');
// 📧 Email to user@test.com: Server is down!
// 📱 SMS to +7-999-123-45-67: Server is down!
// 💬 Slack to #alerts: Server is down!
// ✈️ Telegram to 123456789: Server is down!

🌍 Реальные примеры использования

🔥 Где встречается Decorator

PSR-15 Middleware
Каждый middleware оборачивает следующий обработчик
Streams в PHP
GzipStream оборачивает FileStream
Логирование
Добавление timestamp, context к логам
Кэширование
CacheDecorator оборачивает Repository
PHP Decorator для кэширования
// Интерфейс репозитория
interface UserRepositoryInterface
{
    public function findById(int $id): ?User;
}

// Базовая реализация
class UserRepository implements UserRepositoryInterface
{
    public function findById(int $id): ?User
    {
        // Медленный запрос к БД
        return User::query()->find($id);
    }
}

// Декоратор с кэшированием
class CachedUserRepository implements UserRepositoryInterface
{
    public function __construct(
        private UserRepositoryInterface $repository,
        private CacheInterface $cache,
        private int $ttl = 3600,
    ) {}

    public function findById(int $id): ?User
    {
        $key = "user:{$id}";

        // Пробуем получить из кэша
        $cached = $this->cache->get($key);

        if ($cached !== null) {
            return $cached;
        }

        // Кэша нет — идём в БД
        $user = $this->repository->findById($id);

        if ($user !== null) {
            $this->cache->set($key, $user, $this->ttl);
        }

        return $user;
    }
}

// Использование
$repository = new CachedUserRepository(
    new UserRepository(),
    new RedisCache(),
);

$user = $repository->findById(1); // БД
$user = $repository->findById(1); // Кэш!
💡 Преимущества Decorator
  • Добавляем функционал без изменения существующего кода (OCP)
  • Комбинируем поведения в любом порядке
  • Каждый декоратор делает одно дело (SRP)
  • Легко тестировать по отдельности

🔌 Adapter — адаптер

🔌

Adapter

Структурный паттерн
Adapter позволяет объектам с несовместимыми интерфейсами работать вместе. Он оборачивает один объект и предоставляет интерфейс, ожидаемый клиентом.

🎯 Когда использовать

Интеграция со сторонним API/библиотекой
Замена одного сервиса на другой
Работа с legacy-кодом
Унификация разных интерфейсов

❌ Проблема без Adapter

Вы используете один платёжный шлюз, а потом решаете добавить второй. Но у них совершенно разные API. Без адаптера придётся менять код везде.

PHP Без паттерна — жёсткая привязка
// Stripe API
class StripeApi
{
    public function createCharge(array $data): object
    {
        // Stripe использует cents и свой формат
        return (object) ['id' => 'ch_xxx', 'status' => 'succeeded'];
    }
}

// PayPal API — совсем другой интерфейс!
class PayPalApi
{
    public function makePayment(float $amount, string $currency): array
    {
        // PayPal использует доллары и другой формат ответа
        return ['payment_id' => 'PAY-xxx', 'state' => 'approved'];
    }
}

// Код пронизан условиями — ужас!
class PaymentService
{
    public function pay(string $gateway, float $amount): bool
    {
        if ($gateway === 'stripe') {
            $stripe = new StripeApi();
            $result = $stripe->createCharge([
                'amount' => $amount * 100, // в центах
                'currency' => 'usd',
            ]);
            return $result->status === 'succeeded';
        }

        if ($gateway === 'paypal') {
            $paypal = new PayPalApi();
            $result = $paypal->makePayment($amount, 'USD');
            return $result['state'] === 'approved';
        }

        // 💥 Добавить новый шлюз = менять этот код
        throw new Exception('Unknown gateway');
    }
}

✅ Решение с Adapter

Структура паттерна Adapter
Клиент
Адаптер
Сторонний сервис

Примеры кода

PHP Adapter Pattern
// Единый интерфейс для всех платёжных систем
interface PaymentGatewayInterface
{
    public function charge(float $amount, string $currency): PaymentResult;
    public function refund(string $transactionId): bool;
}

// Результат платежа — унифицированный
readonly class PaymentResult
{
    public function __construct(
        public bool $success,
        public string $transactionId,
        public ?string $errorMessage = null,
    ) {}
}

// Сторонний Stripe SDK (мы не можем его изменить)
class StripeSDK
{
    public function createCharge(int $amountInCents, string $currency): object
    {
        // Stripe работает с центами!
        return (object) [
            'id' => 'ch_' . uniqid(),
            'status' => 'succeeded',
            'amount' => $amountInCents,
        ];
    }

    public function createRefund(string $chargeId): object
    {
        return (object) ['status' => 'succeeded'];
    }
}

// Сторонний PayPal SDK (другой интерфейс!)
class PayPalSDK
{
    public function makePayment(float $amount, string $currencyCode): array
    {
        // PayPal работает с долларами и массивами
        return [
            'payment_id' => 'PAY-' . uniqid(),
            'state' => 'approved',
        ];
    }

    public function refundPayment(string $paymentId): array
    {
        return ['state' => 'completed'];
    }
}
PHP Адаптеры для каждого шлюза
// Адаптер для Stripe
class StripeAdapter implements PaymentGatewayInterface
{
    public function __construct(
        private StripeSDK $stripe,
    ) {}

    public function charge(float $amount, string $currency): PaymentResult
    {
        // Конвертируем доллары в центы для Stripe
        $amountInCents = (int) ($amount * 100);

        try {
            $result = $this->stripe->createCharge(
                $amountInCents,
                strtolower($currency)
            );

            return new PaymentResult(
                success: $result->status === 'succeeded',
                transactionId: $result->id,
            );
        } catch (Exception $e) {
            return new PaymentResult(
                success: false,
                transactionId: '',
                errorMessage: $e->getMessage(),
            );
        }
    }

    public function refund(string $transactionId): bool
    {
        $result = $this->stripe->createRefund($transactionId);
        return $result->status === 'succeeded';
    }
}

// Адаптер для PayPal
class PayPalAdapter implements PaymentGatewayInterface
{
    public function __construct(
        private PayPalSDK $paypal,
    ) {}

    public function charge(float $amount, string $currency): PaymentResult
    {
        try {
            $result = $this->paypal->makePayment(
                $amount,
                strtoupper($currency)
            );

            return new PaymentResult(
                success: $result['state'] === 'approved',
                transactionId: $result['payment_id'],
            );
        } catch (Exception $e) {
            return new PaymentResult(
                success: false,
                transactionId: '',
                errorMessage: $e->getMessage(),
            );
        }
    }

    public function refund(string $transactionId): bool
    {
        $result = $this->paypal->refundPayment($transactionId);
        return $result['state'] === 'completed';
    }
}
PHP Использование
// Сервис работает с интерфейсом — не знает о реализации
class PaymentService
{
    public function __construct(
        private PaymentGatewayInterface $gateway,
    ) {}

    public function processOrder(Order $order): PaymentResult
    {
        // Один и тот же код для любого шлюза!
        return $this->gateway->charge(
            $order->total,
            $order->currency,
        );
    }
}

// Конфигурация в DI-контейнере
$gateway = match (config('payment.default')) {
    'stripe' => new StripeAdapter(new StripeSDK()),
    'paypal' => new PayPalAdapter(new PayPalSDK()),
    default => throw new Exception('Unknown gateway'),
};

$paymentService = new PaymentService($gateway);

// Использование
$result = $paymentService->processOrder($order);

if ($result->success) {
    echo "Оплата прошла! ID: {$result->transactionId}";
} else {
    echo "Ошибка: {$result->errorMessage}";
}

🌍 Реальные примеры использования

🔥 Где встречается Adapter

Laravel Filesystem
Адаптеры для S3, FTP, Local через Flysystem
Логгеры PSR-3
Адаптеры Monolog для разных хранилищ
Кэширование PSR-6/16
Адаптеры для Redis, Memcached, File
ORM
Адаптеры для MySQL, PostgreSQL, SQLite
PHP Adapter для логирования
// Ваш интерфейс логирования
interface LoggerInterface
{
    public function log(string $level, string $message, array $context = []): void;
}

// Сторонний логгер с другим интерфейсом
class ThirdPartyLogger
{
    public function writeLog(string $msg, int $severity): void
    {
        echo "[{$severity}] {$msg}\n";
    }
}

// Адаптер
class ThirdPartyLoggerAdapter implements LoggerInterface
{
    private const LEVEL_MAP = [
        'debug' => 0,
        'info' => 1,
        'warning' => 2,
        'error' => 3,
    ];

    public function __construct(
        private ThirdPartyLogger $logger,
    ) {}

    public function log(string $level, string $message, array $context = []): void
    {
        // Преобразуем формат
        $severity = self::LEVEL_MAP[$level] ?? 1;
        $formattedMessage = $this->formatMessage($message, $context);

        $this->logger->writeLog($formattedMessage, $severity);
    }

    private function formatMessage(string $message, array $context): string
    {
        foreach ($context as $key => $value) {
            $message = str_replace("{{$key}}", $value, $message);
        }
        return $message;
    }
}
💡 Преимущества Adapter
  • Изолирует бизнес-логику от внешних зависимостей
  • Легко заменять сторонние сервисы
  • Упрощает тестирование (можно подставить мок)
  • Следует принципу инверсии зависимостей (DIP)

🏗️ Builder — строитель

🏗️

Builder

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

🎯 Когда использовать

Объект имеет много параметров конструктора
Многие параметры опциональны
Нужна валидация при создании
Хотите fluent interface (цепочка вызовов)

❌ Проблема без Builder

Представьте класс с 10+ параметрами в конструкторе. Половина из них опциональна. Код создания объекта становится нечитаемым.

PHP Без паттерна — телескопический конструктор
class Email
{
    public function __construct(
        private string $to,
        private string $subject,
        private string $body,
        private ?string $from = null,
        private ?string $replyTo = null,
        private array $cc = [],
        private array $bcc = [],
        private array $attachments = [],
        private array $headers = [],
        private ?string $htmlBody = null,
        private int $priority = 3,
        private bool $trackOpens = false,
        private bool $trackClicks = false,
    ) {}
}

// 💥 Создание объекта — кошмар!
$email = new Email(
    'user@test.com',
    'Hello',
    'Body text',
    'noreply@site.com',
    null,                // replyTo — что это?
    ['manager@site.com'],
    [],                  // bcc — пустой
    [],                  // attachments — пустой
    [],                  // headers — пустой
    '<h1>Hello</h1>',
    1,                   // priority — что значит 1?
    true,
    false,               // 🤯 Какой параметр это?
);

✅ Решение с Builder

Структура паттерна Builder
Builder
step1()
step2()
build()

Примеры кода

PHP Builder Pattern
// Immutable объект Email
readonly class Email
{
    public function __construct(
        public string $to,
        public string $subject,
        public string $body,
        public ?string $from = null,
        public ?string $replyTo = null,
        public array $cc = [],
        public array $bcc = [],
        public array $attachments = [],
        public array $headers = [],
        public ?string $htmlBody = null,
        public int $priority = 3,
        public bool $trackOpens = false,
        public bool $trackClicks = false,
    ) {}

    // Статический метод для получения Builder
    public static function builder(): EmailBuilder
    {
        return new EmailBuilder();
    }
}

// Builder
class EmailBuilder
{
    private string $to;
    private string $subject;
    private string $body;
    private ?string $from = null;
    private ?string $replyTo = null;
    private array $cc = [];
    private array $bcc = [];
    private array $attachments = [];
    private array $headers = [];
    private ?string $htmlBody = null;
    private int $priority = 3;
    private bool $trackOpens = false;
    private bool $trackClicks = false;

    public function to(string $to): self
    {
        $this->to = $to;
        return $this;
    }

    public function subject(string $subject): self
    {
        $this->subject = $subject;
        return $this;
    }

    public function body(string $body): self
    {
        $this->body = $body;
        return $this;
    }

    public function from(string $from): self
    {
        $this->from = $from;
        return $this;
    }

    public function replyTo(string $replyTo): self
    {
        $this->replyTo = $replyTo;
        return $this;
    }

    public function cc(string ...$emails): self
    {
        $this->cc = $emails;
        return $this;
    }

    public function bcc(string ...$emails): self
    {
        $this->bcc = $emails;
        return $this;
    }

    public function attach(string $path, ?string $name = null): self
    {
        $this->attachments[] = ['path' => $path, 'name' => $name];
        return $this;
    }

    public function header(string $name, string $value): self
    {
        $this->headers[$name] = $value;
        return $this;
    }

    public function html(string $html): self
    {
        $this->htmlBody = $html;
        return $this;
    }

    public function highPriority(): self
    {
        $this->priority = 1;
        return $this;
    }

    public function lowPriority(): self
    {
        $this->priority = 5;
        return $this;
    }

    public function withTracking(): self
    {
        $this->trackOpens = true;
        $this->trackClicks = true;
        return $this;
    }

    public function build(): Email
    {
        // Валидация перед созданием
        $this->validate();

        return new Email(
            to: $this->to,
            subject: $this->subject,
            body: $this->body,
            from: $this->from,
            replyTo: $this->replyTo,
            cc: $this->cc,
            bcc: $this->bcc,
            attachments: $this->attachments,
            headers: $this->headers,
            htmlBody: $this->htmlBody,
            priority: $this->priority,
            trackOpens: $this->trackOpens,
            trackClicks: $this->trackClicks,
        );
    }

    private function validate(): void
    {
        if (!isset($this->to) || !filter_var($this->to, FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Valid "to" email is required');
        }

        if (!isset($this->subject) || empty($this->subject)) {
            throw new InvalidArgumentException('Subject is required');
        }

        if (!isset($this->body) || empty($this->body)) {
            throw new InvalidArgumentException('Body is required');
        }
    }
}
PHP Использование
// Простое письмо — читаемо и понятно!
$email = Email::builder()
    ->to('user@test.com')
    ->subject('Welcome!')
    ->body('Thank you for registration.')
    ->build();

// Сложное письмо с вложениями и трекингом
$email = Email::builder()
    ->to('client@company.com')
    ->from('sales@oursite.com')
    ->replyTo('support@oursite.com')
    ->subject('Your Invoice #12345')
    ->body('Please find your invoice attached.')
    ->html('<h1>Invoice #12345</h1><p>See attachment.</p>')
    ->cc('manager@company.com', 'accountant@company.com')
    ->attach('/invoices/12345.pdf', 'Invoice.pdf')
    ->header('X-Campaign-ID', 'invoice-2024')
    ->highPriority()
    ->withTracking()
    ->build();

// Каждый шаг понятен без комментариев!

Пример: Query Builder

PHP SQL Query Builder
class QueryBuilder
{
    private string $table;
    private array $select = ['*'];
    private array $where = [];
    private array $orderBy = [];
    private ?int $limit = null;
    private ?int $offset = null;
    private array $joins = [];
    private array $bindings = [];

    public function table(string $table): self
    {
        $this->table = $table;
        return $this;
    }

    public function select(string ...$columns): self
    {
        $this->select = $columns;
        return $this;
    }

    public function where(string $column, mixed $value, string $operator = '='): self
    {
        $this->where[] = [$column, $operator, '?'];
        $this->bindings[] = $value;
        return $this;
    }

    public function whereIn(string $column, array $values): self
    {
        $placeholders = implode(', ', array_fill(0, count($values), '?'));
        $this->where[] = [$column, 'IN', "({$placeholders})"];
        $this->bindings = array_merge($this->bindings, $values);
        return $this;
    }

    public function join(string $table, string $first, string $second): self
    {
        $this->joins[] = "JOIN {$table} ON {$first} = {$second}";
        return $this;
    }

    public function leftJoin(string $table, string $first, string $second): self
    {
        $this->joins[] = "LEFT JOIN {$table} ON {$first} = {$second}";
        return $this;
    }

    public function orderBy(string $column, string $direction = 'ASC'): self
    {
        $this->orderBy[] = "{$column} {$direction}";
        return $this;
    }

    public function limit(int $limit): self
    {
        $this->limit = $limit;
        return $this;
    }

    public function offset(int $offset): self
    {
        $this->offset = $offset;
        return $this;
    }

    public function toSql(): string
    {
        $sql = 'SELECT ' . implode(', ', $this->select);
        $sql .= ' FROM ' . $this->table;

        if (!empty($this->joins)) {
            $sql .= ' ' . implode(' ', $this->joins);
        }

        if (!empty($this->where)) {
            $conditions = array_map(
                fn($w) => "{$w[0]} {$w[1]} {$w[2]}",
                $this->where
            );
            $sql .= ' WHERE ' . implode(' AND ', $conditions);
        }

        if (!empty($this->orderBy)) {
            $sql .= ' ORDER BY ' . implode(', ', $this->orderBy);
        }

        if ($this->limit !== null) {
            $sql .= ' LIMIT ' . $this->limit;
        }

        if ($this->offset !== null) {
            $sql .= ' OFFSET ' . $this->offset;
        }

        return $sql;
    }

    public function getBindings(): array
    {
        return $this->bindings;
    }
}

// Использование
$query = (new QueryBuilder())
    ->table('users')
    ->select('users.id', 'users.name', 'orders.total')
    ->leftJoin('orders', 'users.id', 'orders.user_id')
    ->where('users.status', 'active')
    ->where('users.age', 18, '>=')
    ->whereIn('users.role', ['admin',->whereIn('users.role', ['admin', 'moderator', 'editor'])
    ->orderBy('users.created_at', 'DESC')
    ->limit(20)
    ->offset(0);

echo $query->toSql();
// SELECT users.id, users.name, orders.total
// FROM users
// LEFT JOIN orders ON users.id = orders.user_id
// WHERE users.status = ? AND users.age >= ? AND users.role IN (?, ?, ?)
// ORDER BY users.created_at DESC
// LIMIT 20 OFFSET 0

print_r($query->getBindings());
// ['active', 18, 'admin', 'moderator', 'editor']

🌍 Реальные примеры использования

🔥 Где встречается Builder

Laravel Query Builder
DB::table('users')->where()->get()
Symfony Form Builder
$builder->add('name', TextType::class)
PHPUnit Mocks
$mock->method('x')->willReturn('y')
HTTP Client
Guzzle, Symfony HttpClient
PHP HTTP Request Builder
class HttpRequestBuilder
{
    private string $method = 'GET';
    private string $url;
    private array $headers = [];
    private array $query = [];
    private mixed $body = null;
    private int $timeout = 30;
    private bool $verifySSL = true;
    private ?string $bearerToken = null;

    public static function create(): self
    {
        return new self();
    }

    public function get(string $url): self
    {
        $this->method = 'GET';
        $this->url = $url;
        return $this;
    }

    public function post(string $url): self
    {
        $this->method = 'POST';
        $this->url = $url;
        return $this;
    }

    public function put(string $url): self
    {
        $this->method = 'PUT';
        $this->url = $url;
        return $this;
    }

    public function delete(string $url): self
    {
        $this->method = 'DELETE';
        $this->url = $url;
        return $this;
    }

    public function withHeader(string $name, string $value): self
    {
        $this->headers[$name] = $value;
        return $this;
    }

    public function withBearerToken(string $token): self
    {
        $this->bearerToken = $token;
        return $this;
    }

    public function withQuery(array $params): self
    {
        $this->query = array_merge($this->query, $params);
        return $this;
    }

    public function withJson(array $data): self
    {
        $this->body = json_encode($data);
        $this->headers['Content-Type'] = 'application/json';
        return $this;
    }

    public function withFormData(array $data): self
    {
        $this->body = http_build_query($data);
        $this->headers['Content-Type'] = 'application/x-www-form-urlencoded';
        return $this;
    }

    public function timeout(int $seconds): self
    {
        $this->timeout = $seconds;
        return $this;
    }

    public function withoutSSLVerification(): self
    {
        $this->verifySSL = false;
        return $this;
    }

    public function send(): HttpResponse
    {
        // Подготовка заголовков
        $headers = $this->headers;

        if ($this->bearerToken) {
            $headers['Authorization'] = 'Bearer ' . $this->bearerToken;
        }

        // Подготовка URL с query параметрами
        $url = $this->url;
        if (!empty($this->query)) {
            $url .= '?' . http_build_query($this->query);
        }

        // Здесь логика отправки запроса (cURL, Guzzle и т.д.)
        return new HttpResponse(/* ... */);
    }
}

// Использование — очень читаемо!
$response = HttpRequestBuilder::create()
    ->post('https://api.example.com/users')
    ->withBearerToken($token)
    ->withHeader('X-Request-ID', uniqid())
    ->withJson([
        'name' => 'John Doe',
        'email' => 'john@example.com',
    ])
    ->timeout(10)
    ->send();

// GET запрос с параметрами
$response = HttpRequestBuilder::create()
    ->get('https://api.example.com/search')
    ->withQuery([
        'q' => 'php patterns',
        'page' => 1,
        'limit' => 20,
    ])
    ->withBearerToken($token)
    ->send();
💡 Преимущества Builder
  • Читаемый код — каждый шаг понятен
  • Опциональные параметры не требуют null
  • Валидация при сборке (fail fast)
  • Можно создавать разные конфигурации
  • IDE подсказывает доступные методы

📊 Сравнение и выводы

Когда какой паттерн использовать

🎀 Decorator

Добавить поведение к существующему объекту, не меняя его код. Комбинировать поведения.

🔌 Adapter

Использовать класс с несовместимым интерфейсом. Интеграция со сторонними сервисами.

🏗️ Builder

Создать сложный объект пошагово. Много опциональных параметров.

Критерий Decorator Adapter Builder
Тип паттерна Структурный Структурный Порождающий
Основная цель Добавить функционал Совместить интерфейсы Создать объект
Когда применять Нужно расширить поведение Интеграция с внешним API Сложный конструктор
Пример в жизни Middleware, кэш-обёртка Платёжные шлюзы Query Builder, Email
Сохраняет интерфейс ✅ Да ❌ Преобразует ➖ Создаёт новый объект
Сложность Средняя Низкая Средняя

🎯 Ключевые выводы

🎀

Decorator

Оборачиваем объект в «обёртки» для добавления новых возможностей без изменения исходного кода

🔌

Adapter

Создаём «переходник» между несовместимыми интерфейсами для бесшовной интеграции

🏗️

Builder

Строим сложные объекты пошагово с fluent interface и валидацией

📝 Практические советы
  • Decorator: Используйте для middleware, кэширования, логирования, добавления функций «на лету»
  • Adapter: Всегда оборачивайте сторонние библиотеки — это упростит их замену и тестирование
  • Builder: Если конструктор имеет больше 3-4 параметров — рассмотрите Builder
  • Паттерны можно комбинировать: Builder может создавать объекты, которые потом оборачиваются Decorator'ами

Что изучить дальше

  • Facade — упрощённый интерфейс к сложной подсистеме
  • Proxy — заместитель объекта для контроля доступа
  • Composite — древовидная структура объектов
  • Abstract Factory — семейства связанных объектов
  • Prototype — клонирование объектов
👨‍💻

Об авторе

Практическое руководство по паттернам проектирования. Пишу о PHP, архитектуре и чистом коде.

📚 Читать другие статьи