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 даёт больше контроля над типами, что помогает избежать ошибок на этапе компиляции.
Типичные ошибки и подводные камни
- Злоупотребление Singleton
Многие новички делают синглтоном всё подряд. Помни: Singleton — это не просто «глобальная переменная». Если ты используешь его для всего, код становится жёстко связанным и его невозможно тестировать.
Правило: используй Singleton только когда объект действительно должен быть один (например, подключение к БД, логгер).
- Сложность там, где не нужно
«У нас две строчки кода? Давай навесим паттерн Factory!» — типичная ошибка. Паттерны нужны для решения сложных проблем, а не для того, чтобы «было красиво».
- Нарушение принципа единственной ответственности
Когда один класс делает слишком много — это нарушает SRP (Single Responsibility Principle). Паттерны должны помогать разделять обязанности, а не смешивать их.
- Игнорирование интерфейсов
В PHP без интерфейсов паттерны теряют смысл. Если ты не используешь
interfaceилиabstract class, то ты не реализуешь паттерн, а просто копируешь код. - Проблемы с памятью в Observer
Если не отписывать наблюдателей, они остаются в памяти и вызывают утечки. Всегда реализуй метод
detach()и вызывай его, когда подписка больше не нужна. - Слишком глубокая вложенность в Decorator
10 декораторов, обёрнутых друг в друга — это кошмар для отладки. Старайся не делать больше 3-4