🐘 PHP

أنماط التصميم في PHP — دليل شامل

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты строишь дом. Можно каждый раз изобретать велосипед: придумывать, как соединять брёвна, как делать крышу, как прокладывать проводку. А можно использовать проверенные веками решения: фундамент — ленточный, крыша — двускатная, окна — с двойными рамами. Эти проверенные решения и есть паттерны проектирования в программировании.

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

Зачем они нужны? Представь, что ты работаешь в команде из 10 программистов. Если каждый будет писать код как бог на душу положит, получится каша. А если все используют одни и те же паттерны, то любой разработчик, заходя в проект, сразу понимает: «Ага, здесь Singleton, тут Factory, а это Observer». Это как говорить на одном языке.

Паттерны — это не магия и не панацея. Это инструменты. Молотком можно забить гвоздь, а можно разбить окно. Всё зависит от того, где и как ты его применяешь.

Теория: как это работает изнутри

В мире объектно-ориентированного программирования (ООП) существует 23 классических паттерна, описанных «Бандой четырёх» (GoF — Gang of Four). Они делятся на три группы:

Группа Что делает Примеры
Порождающие Отвечают за создание объектов. Помогают сделать процесс создания гибким и независимым от конкретных классов. Singleton, Factory, Builder, Prototype
Структурные Определяют, как классы и объекты собираются в более крупные структуры. Adapter, Decorator, Facade, Proxy
Поведенческие Определяют алгоритмы и распределение обязанностей между объектами. Observer, Strategy, Command, Iterator

Как это работает изнутри? Каждый паттерн — это набор правил:

  • Контекст — в какой ситуации применяется
  • Проблема — что именно нужно решить
  • Решение — схема классов и их взаимодействие

Например, паттерн Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. Внутри это работает так: конструктор делается приватным (чтобы никто не мог создать объект через new), а для получения объекта используется статический метод, который либо создаёт объект в первый раз, либо возвращает уже существующий.

Синтаксис в PHP: все формы и варианты

PHP — язык, который отлично подходит для реализации паттернов благодаря своей гибкости. Вот основные конструкции, которые тебе понадобятся:

Конструкция Описание Пример использования в паттернах
private function __construct() Приватный конструктор — запрещает создание объекта через new Singleton
private function __clone() Приватный метод клонирования — запрещает копирование объекта Singleton
static function getInstance() Статический метод для получения единственного экземпляра Singleton
interface Определяет контракт — какие методы должен реализовать класс Strategy, Observer
abstract class Абстрактный класс — частичная реализация, которую нужно дополнить Factory Method
trait Примесь — повторное использование кода без наследования Decorator
__invoke() Магический метод, позволяющий вызывать объект как функцию Strategy
Closure Анонимные функции (замыкания) — можно передавать как параметры Command, Observer
В PHP 8+ появились атрибуты (аннотации), которые тоже можно использовать для реализации некоторых паттернов, но это уже продвинутая тема.

Примеры кода — от простого к сложному

Пример 1: Singleton (Одиночка) — простейший

<?php
/**
 * Паттерн Singleton (Одиночка)
 * Гарантирует, что у класса есть только один экземпляр
 * 
 * Аналогия: президент страны — он один, и все знают, как к нему обратиться
 */

class DatabaseConnection {
    // Статическое свойство хранит единственный экземпляр класса
    private static ?DatabaseConnection $instance = null;
    
    // Приватный конструктор — никто не может создать объект через new
    private function __construct() {
        echo "Создаю новое подключение к базе данных...
";
    }
    
    // Приватный метод клонирования — запрещаем копирование
    private function __clone() {}
    
    // Приватный метод wakeup — запрещаем восстановление из сериализации
    public function __wakeup() {
        throw new Exception("Нельзя десериализовать Singleton");
    }
    
    // Статический метод — единственный способ получить экземпляр
    public static function getInstance(): DatabaseConnection {
        // Если экземпляр ещё не создан — создаём
        if (self::$instance === null) {
            self::$instance = new self();
        }
        // Возвращаем существующий экземпляр
        return self::$instance;
    }
    
    // Обычный метод для работы с базой
    public function query(string $sql): string {
        return "Выполняю запрос: " . $sql;
    }
}

// Использование
$db1 = DatabaseConnection::getInstance();
$db2 = DatabaseConnection::getInstance();

// Проверяем, что это один и тот же объект
if ($db1 === $db2) {
    echo "Это один и тот же объект!
";
}

echo $db1->query("SELECT * FROM users");
?>

Пример 2: Factory Method (Фабричный метод)

<?php
/**
 * Паттерн Factory Method (Фабричный метод)
 * Определяет интерфейс для создания объекта, но позволяет подклассам 
 * изменять тип создаваемых объектов
 * 
 * Аналогия: пиццерия — ты заказываешь пиццу, а какой именно рецепт 
 * использовать, решает повар (фабрика)
 */

// Интерфейс для всех продуктов (пицц)
interface Pizza {
    public function getName(): string;
    public function getIngredients(): array;
}

// Конкретные продукты
class MargheritaPizza implements Pizza {
    public function getName(): string {
        return "Маргарита";
    }
    
    public function getIngredients(): array {
        return ["томатный соус", "моцарелла", "базилик"];
    }
}

class PepperoniPizza implements Pizza {
    public function getName(): string {
        return "Пепперони";
    }
    
    public function getIngredients(): array {
        return ["томатный соус", "моцарелла", "пепперони"];
    }
}

// Абстрактный создатель (фабрика)
abstract class PizzaFactory {
    // Фабричный метод — его переопределяют подклассы
    abstract public function createPizza(string $type): Pizza;
    
    // Общий метод для всех фабрик
    public function orderPizza(string $type): string {
        // Создаём пиццу через фабричный метод
        $pizza = $this->createPizza($type);
        
        // Готовим пиццу
        $result = "Готовим пиццу " . $pizza->getName() . "
";
        $result .= "Ингредиенты: " . implode(", ", $pizza->getIngredients()) . "
";
        $result .= "Пицца готова!
";
        
        return $result;
    }
}

// Конкретная фабрика
class ItalianPizzaFactory extends PizzaFactory {
    // Реализуем фабричный метод
    public function createPizza(string $type): Pizza {
        return match($type) {
            'margherita' => new MargheritaPizza(),
            'pepperoni' => new PepperoniPizza(),
            default => throw new InvalidArgumentException("Неизвестный тип пиццы")
        };
    }
}

// Использование
$factory = new ItalianPizzaFactory();
echo $factory->orderPizza('margherita');
?>

Пример 3: Builder (Строитель)

<?php
/**
 * Паттерн Builder (Строитель)
 * Позволяет создавать сложные объекты пошагово
 * 
 * Аналогия: сборка компьютера — ты выбираешь процессор, видеокарту, 
 * память, а строитель собирает всё в готовый ПК
 */

// Продукт — сложный объект, который мы собираем
class Computer {
    private string $cpu = '';
    private string $gpu = '';
    private int $ram = 0;
    private int $storage = 0;
    private string $os = '';
    
    // Методы для установки компонентов
    public function setCpu(string $cpu): void {
        $this->cpu = $cpu;
    }
    
    public function setGpu(string $gpu): void {
        $this->gpu = $gpu;
    }
    
    public function setRam(int $ram): void {
        $this->ram = $ram;
    }
    
    public function setStorage(int $storage): void {
        $this->storage = $storage;
    }
    
    public function setOs(string $os): void {
        $this->os = $os;
    }
    
    public function getSpecs(): string {
        return "CPU: {$this->cpu}
GPU: {$this->gpu}
RAM: {$this->ram}GB
" .
               "Storage: {$this->storage}GB
OS: {$this->os}";
    }
}

// Интерфейс строителя
interface ComputerBuilder {
    public function reset(): void;
    public function setCpu(string $cpu): self;
    public function setGpu(string $gpu): self;
    public function setRam(int $ram): self;
    public function setStorage(int $storage): self;
    public function setOs(string $os): self;
    public function build(): Computer;
}

// Конкретный строитель
class GamingComputerBuilder implements ComputerBuilder {
    private Computer $computer;
    
    public function reset(): void {
        $this->computer = new Computer();
    }
    
    public function setCpu(string $cpu): self {
        $this->computer->setCpu($cpu);
        return $this; // Возвращаем $this для цепочки вызовов
    }
    
    public function setGpu(string $gpu): self {
        $this->computer->setGpu($gpu);
        return $this;
    }
    
    public function setRam(int $ram): self {
        $this->computer->setRam($ram);
        return $this;
    }
    
    public function setStorage(int $storage): self {
        $this->computer->setStorage($storage);
        return $this;
    }
    
    public function setOs(string $os): self {
        $this->computer->setOs($os);
        return $this;
    }
    
    public function build(): Computer {
        $result = $this->computer;
        $this->reset(); // Сбрасываем строитель для следующего использования
        return $result;
    }
}

// Директор (опционально) — управляет последовательностью сборки
class ComputerDirector {
    public function buildBudgetPC(ComputerBuilder $builder): Computer {
        return $builder
            ->reset()
            ->setCpu("Intel i3")
            ->setGpu("Integrated")
            ->setRam(8)
            ->setStorage(256)
            ->setOs("Windows 11 Home")
            ->build();
    }
    
    public function buildGamingPC(ComputerBuilder $builder): Computer {
        return $builder
            ->reset()
            ->setCpu("Intel i9")
            ->setGpu("RTX 4090")
            ->setRam(64)
            ->setStorage(2000)
            ->setOs("Windows 11 Pro")
            ->build();
    }
}

// Использование
$builder = new GamingComputerBuilder();
$director = new ComputerDirector();

$gamingPC = $director->buildGamingPC($builder);
echo "Игровой ПК:
" . $gamingPC->getSpecs() . "

";

$budgetPC = $director->buildBudgetPC($builder);
echo "Бюджетный ПК:
" . $budgetPC->getSpecs();
?>

Пример 4: Adapter (Адаптер)

<?php
/**
 * Паттерн Adapter (Адаптер)
 * Позволяет объектам с несовместимыми интерфейсами работать вместе
 * 
 * Аналогия: переходник для розетки — у тебя вилка с двумя штырьками, 
 * а розетка с тремя дырками. Адаптер делает их совместимыми
 */

// Целевой интерфейс — то, что мы хотим получить
interface NotificationService {
    public function send(string $title, string $message): string;
}

// Существующий класс с несовместимым интерфейсом
class OldEmailSystem {
    // У этого метода другой формат параметров
    public function sendEmail(string $to, string $subject, string $body): string {
        return "Отправлено email на {$to}: {$subject} - {$body}";
    }
}

// Адаптер — делает старую систему совместимой с новым интерфейсом
class EmailAdapter implements NotificationService {
    private OldEmailSystem $oldSystem;
    private string $defaultEmail;
    
    public function __construct(OldEmailSystem $oldSystem, string $defaultEmail) {
        $this->oldSystem = $oldSystem;
        $this->defaultEmail = $defaultEmail;
    }
    
    // Адаптируем метод send под старую систему
    public function send(string $title, string $message): string {
        // Преобразуем вызов: title -> subject, message -> body
        return $this->oldSystem->sendEmail(
            $this->defaultEmail,
            $title,
            $message
        );
    }
}

// Ещё один адаптер для SMS
class SmsAdapter implements NotificationService {
    private string $phoneNumber;
    
    public function __construct(string $phoneNumber) {
        $this->phoneNumber = $phoneNumber;
    }
    
    public function send(string $title, string $message): string {
        // Отправляем SMS (упрощённо)
        return "Отправлено SMS на {$this->phoneNumber}: {$title}: {$message}";
    }
}

// Клиентский код — работает только с NotificationService
class NotificationManager {
    private array $services = [];
    
    public function addService(NotificationService $service): void {
        $this->services[] = $service;
    }
    
    public function sendNotification(string $title, string $message): array {
        $results = [];
        foreach ($this->services as $service) {
            $results[] = $service->send($title, $message);
        }
        return $results;
    }
}

// Использование
$manager = new NotificationManager();

// Добавляем адаптированную старую систему
$oldEmail = new OldEmailSystem();
$emailAdapter = new EmailAdapter($oldEmail, "user@example.com");
$manager->addService($emailAdapter);

// Добавляем SMS
$smsAdapter = new SmsAdapter("+7-999-123-45-67");
$manager->addService($smsAdapter);

// Отправляем уведомление через все сервисы
$results = $manager->sendNotification("Важный апдейт", "Обновите пароль!");
foreach ($results as $result) {
    echo $result . "
";
}
?>

Пример 5: Observer (Наблюдатель)

<?php
/**
 * Паттерн Observer (Наблюдатель)
 * Создаёт механизм подписки, позволяющий одним объектам следить 
 * за изменениями других
 * 
 * Аналогия: YouTube — ты подписываешься на канал, и когда выходит 
 * новое видео, ты получаешь уведомление
 */

// Интерфейс наблюдателя (подписчика)
interface Observer {
    public function update(string $eventType, $data): void;
}

// Интерфейс субъекта (издателя)
interface Subject {
    public function attach(Observer $observer): void;
    public function detach(Observer $observer): void;
    public function notify(string $eventType, $data): void;
}

// Конкретный субъект — YouTube канал
class YouTubeChannel implements Subject {
    private string $name;
    private array $observers = [];
    
    public function __construct(string $name) {
        $this->name = $name;
    }
    
    public function attach(Observer $observer): void {
        $this->observers[] = $observer;
        echo "Новый подписчик!
";
    }
    
    public function detach(Observer $observer): void {
        $index = array_search($observer, $this->observers, true);
        if ($index !== false) {
            unset($this->observers[$index]);
            echo "Подписчик отписался.
";
        }
    }
    
    public function notify(string $eventType, $data): void {
        echo "Оповещаем подписчиков о событии: {$eventType}
";
        foreach ($this->observers as $observer) {
            $observer->update($eventType, $data);
        }
    }
    
    // Метод, который запускает уведомления
    public function uploadVideo(string $title): void {
        echo "Канал {$this->name} загрузил видео: {$title}
";
        $this->notify('new_video', [
            'channel' => $this->name,
            'title' => $title
        ]);
    }
}

// Конкретный наблюдатель — пользователь
class User implements Observer {
    private string $name;
    
    public function __construct(string $name) {
        $this->name = $name;
    }
    
    public function update(string $eventType, $data): void {
        if ($eventType === 'new_video') {
            echo "{$this->name}: Новое видео "{$data['title']}" " .
                 "на канале {$data['channel']}! Иду смотреть!
";
        }
    }
}

// Ещё один наблюдатель — модератор
class Moderator implements Observer {
    public function update(string $eventType, $data): void {
        if ($eventType === 'new_video') {
            echo "Модератор: Проверяю видео "{$data['title']}" " .
                 "на соответствие правилам.
";
        }
    }
}

// Использование
$channel = new YouTubeChannel("Программирование для всех");

$user1 = new User("Анна");
$user2 = new User("Пётр");
$moderator = new Moderator();

// Подписываемся
$channel->attach($user1);
$channel->attach($user2);
$channel->attach($moderator);

// Загружаем видео — все подписчики получат уведомление
$channel->uploadVideo("Паттерны проектирования в PHP");

// Отписываем одного
$channel->detach($user2);

// Загружаем ещё одно видео
$channel->uploadVideo("PHP 8.4 новые возможности");
?>

Пример 6: Strategy (Стратегия)

<?php
/**
 * Паттерн Strategy (Стратегия)
 * Определяет семейство алгоритмов, инкапсулирует каждый из них 
 * и делает их взаимозаменяемыми
 * 
 * Аналогия: навигатор — ты можешь выбрать маршрут: пешком, 
 * на машине, на велосипеде. Алгоритм расчёта разный, 
 * но интерфейс один — проложить маршрут
 */

// Интерфейс стратегии
interface RouteStrategy {
    public function calculateRoute(string $from, string $to): array;
}

// Конкретные стратегии
class WalkingStrategy implements RouteStrategy {
    public function calculateRoute(string $from, string $to): array {
        return [
            'type' => 'пешком',
            'distance' => '2.5 км',
            'time' => '30 минут',
            'description' => "Идите прямо по ул. Ленина, затем поверните на ул. Гагарина"
        ];
    }
}

class DrivingStrategy implements RouteStrategy {
    public function calculateRoute(string $from, string $to): array {
        return [
            'type' => 'на машине',
            'distance' => '5 км',
            'time' => '10 минут',
            'description' => "Выезжайте на МКАД, через 3 км съезд на ул. Пушкина"
        ];
    }
}

class BicycleStrategy implements RouteStrategy {
    public function calculateRoute(string $from, string $to): array {
        return [
            'type' => 'на велосипеде',
            'distance' => '3 км',
            'time' => '15 минут',
            'description' => "Двигайтесь по велодорожке вдоль парка"
        ];
    }
}

// Контекст — использует стратегию
class Navigator {
    private RouteStrategy $strategy;
    
    // Можно установить стратегию через конструктор
    public function __construct(RouteStrategy $strategy) {
        $this->strategy = $strategy;
    }
    
    // Или изменить стратегию в любой момент
    public function setStrategy(RouteStrategy $strategy): void {
        $this->strategy = $strategy;
    }
    
    // Выполняем алгоритм через текущую стратегию
    public function buildRoute(string $from, string $to): string {
        $route = $this->strategy->calculateRoute($from, $to);
        
        return "Маршрут {$route['type']}:
" .
               "Дистанция: {$route['distance']}
" .
               "Время: {$route['time']}
" .
               "Описание: {$route['description']}
";
    }
}

// Использование
$navigator = new Navigator(new WalkingStrategy());
echo $navigator->buildRoute("Дом", "Работа");

// Меняем стратегию на лету
$navigator->setStrategy(new DrivingStrategy());
echo $navigator->buildRoute("Дом", "Работа");

// Используем анонимную функцию как стратегию (альтернативный подход)
$navigator->setStrategy(new class implements RouteStrategy {
    public function calculateRoute(string $from, string $to): array {
        return [
            'type' => 'на такси',
            'distance' => '5 км',
            'time' => '15 минут',
            'description' => "Вызовите такси через приложение"
        ];
    }
});
echo $navigator->buildRoute("Дом", "Аэропорт");
?>

Пример 7: Decorator (Декоратор)

<?php
/**
 * Паттерн Decorator (Декоратор)
 * Позволяет динамически добавлять объектам новую функциональность, 
 * оборачивая их в другие объекты
 * 
 * Аналогия: пицца с добавками — ты берёшь обычную пиццу и 
 * добавляешь сыр, грибы, колбасу. Каждая добавка — это декоратор
 */

// Базовый интерфейс компонента
interface Coffee {
    public function getDescription(): string;
    public function getCost(): float;
}

// Конкретный компонент — базовый кофе
class SimpleCoffee implements Coffee {
    public function getDescription(): string {
        return "Обычный кофе";
    }
    
    public function getCost(): float {
        return 100.0; // 100 рублей
    }
}

// Базовый декоратор — реализует тот же интерфейс
abstract class CoffeeDecorator implements Coffee {
    protected Coffee $coffee;
    
    public function __construct(Coffee $coffee) {
        $this->coffee = $coffee;
    }
    
    // Делегируем вызовы базовому объекту
    public function getDescription(): string {
        return $this->coffee->getDescription();
    }
    
    public function getCost(): float {
        return $this->coffee->getCost();
    }
}

// Конкретные декораторы
class MilkDecorator extends CoffeeDecorator {
    public function getDescription(): string {
        // Добавляем к описанию базового кофе
        return $this->coffee->getDescription() . " + молоко";
    }
    
    public function getCost(): float {
        // Добавляем стоимость молока
        return $this->coffee->getCost() + 30.0;
    }
}

class SugarDecorator extends CoffeeDecorator {
    public function getDescription(): string {
        return $this->coffee->getDescription() . " + сахар";
    }
    
    public function getCost(): float {
        return $this->coffee->getCost() + 10.0;
    }
}

class WhippedCreamDecorator extends CoffeeDecorator {
    public function getDescription(): string {
        return $this->coffee->getDescription() . " + взбитые сливки";
    }
    
    public function getCost(): float {
        return $this->coffee->getCost() + 50.0;
    }
}

class CaramelDecorator extends CoffeeDecorator {
    public function getDescription(): string {
        return $this->coffee->getDescription() . " + карамель";
    }
    
    public function getCost(): float {
        return $this->coffee->getCost() + 40.0;
    }
}

// Использование — создаём кофе с добавками
$coffee = new SimpleCoffee();
echo "Заказ: " . $coffee->getDescription() . "
";
echo "Цена: " . $coffee->getCost() . " руб.

";

// Оборачиваем в декораторы — как матрёшку
$coffeeWithMilk = new MilkDecorator($coffee);
echo "Заказ: " . $coffeeWithMilk->getDescription() . "
";
echo "Цена: " . $coffeeWithMilk->getCost() . " руб.

";

// Можно комбинировать несколько декораторов
$coffeeWithAll = new CaramelDecorator(
    new WhippedCreamDecorator(
        new SugarDecorator(
            new MilkDecorator($coffee)
        )
    )
);
echo "Заказ: " . $coffeeWithAll->getDescription() . "
";
echo "Цена: " . $coffeeWithAll->getCost() . " руб.
";
?>

Сравнение с Python: чем похоже и чем отличается

Аспект PHP Python
Типизация Строгая (с PHP 7+) — нужно указывать типы параметров и возвращаемых значений Динамическая — типы не указываются, но можно добавить аннотации
Интерфейсы Есть ключевое слово interface, класс может реализовать несколько интерфейсов Нет ключевого слова interface, используются абстрактные классы или протоколы (Protocol)
Singleton Реализуется через приватный конструктор и статический метод Можно через __new__ или модуль (модули в Python — синглтоны по умолчанию)
Decorator Требует создания классов-декораторов Есть встроенный синтаксис @decorator для функций
Observer Реализуется вручную через интерфейсы Есть встроенные события в некоторых фреймворках
Производительность Быстрее для веб-запросов (каждый запрос — новый процесс) Медленнее для веба, но быстрее для научных вычислений
Популярность паттернов Широко используются в фреймворках (Laravel, Symfony) Меньше акцент на классические паттерны, больше на Pythonic way
Главное отличие: в Python многие паттерны реализуются проще благодаря динамической природе языка. Например, декораторы в Python — это встроенная возможность, а в PHP нужно писать целые классы. Но PHP даёт больше контроля над типами, что помогает избежать ошибок на этапе компиляции.

Типичные ошибки и подводные камни

  1. Злоупотребление Singleton

    Многие новички делают синглтоном всё подряд. Помни: Singleton — это не просто «глобальная переменная». Если ты используешь его для всего, код становится жёстко связанным и его невозможно тестировать.

    Правило: используй Singleton только когда объект действительно должен быть один (например, подключение к БД, логгер).
  2. Сложность там, где не нужно

    «У нас две строчки кода? Давай навесим паттерн Factory!» — типичная ошибка. Паттерны нужны для решения сложных проблем, а не для того, чтобы «было красиво».

  3. Нарушение принципа единственной ответственности

    Когда один класс делает слишком много — это нарушает SRP (Single Responsibility Principle). Паттерны должны помогать разделять обязанности, а не смешивать их.

  4. Игнорирование интерфейсов

    В PHP без интерфейсов паттерны теряют смысл. Если ты не используешь interface или abstract class, то ты не реализуешь паттерн, а просто копируешь код.

  5. Проблемы с памятью в Observer

    Если не отписывать наблюдателей, они остаются в памяти и вызывают утечки. Всегда реализуй метод detach() и вызывай его, когда подписка больше не нужна.

  6. Слишком глубокая вложенность в Decorator

    10 декораторов, обёрнутых друг в друга — это кошмар для отладки. Старайся не делать больше 3-4