Паттерны проектирования в PHP: не просто модные слова, а ваш инструмент для чистого кода
Каждый PHP-разработчик рано или поздно сталкивается с ситуацией, когда код превращается в «спагетти»: классы раздуваются, зависимости запутываются, а добавление новой функциональности требует переписывания половины проекта. Знакомо?
Именно здесь на помощь приходят паттерны проектирования в PHP (design patterns) — проверенные временем архитектурные решения типовых задач. Они не являются готовыми библиотеками или кусками кода, которые можно скопировать. Это концепции, которые помогают организовать структуру вашего приложения так, чтобы оно было гибким, тестируемым и легко поддерживаемым.
В этой статье мы разберем не теорию ради теории. Мы посмотрим на реальные примеры кода PHP и ответим на главный вопрос: «Когда этот паттерн действительно нужен, а когда он только усложнит жизнь?».
Почему стоит использовать паттерны проектирования в PHP?
Многие начинающие разработчики считают паттерны излишней абстракцией. Но на практике они решают три ключевые проблемы:
- Снижение связанности (loose coupling) — классы меньше зависят друг от друга, что упрощает их замену и тестирование.
- Повышение переиспользуемости — вы не пишете один и тот же код в разных местах.
- Упрощение рефакторинга — когда требования меняются (а они меняются всегда), паттерн позволяет внести изменения локально, не ломая всю систему.
Но есть нюанс: применять паттерны нужно осознанно. Если у вас простое CRUD-приложение на 3 таблицы, внедрение сложной фабрики или абстрактного строителя — это оверинжиниринг. Паттерн оправдан, когда вы предвидите рост сложности или когда текущая архитектура уже начинает «болеть».
Топ-5 паттернов проектирования в PHP с примерами
1. Singleton (Одиночка) — когда объект должен быть один
Когда использовать: Вам нужен единственный экземпляр класса для всего приложения. Классический пример — подключение к базе данных или логгер. Если вы создадите новое подключение в каждом контроллере, вы рискуете исчерпать лимит соединений.
class Database {
private static ?Database $instance = null;
private PDO $connection;
private function __construct() {
$this->connection = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
}
// Запрещаем клонирование и unserialize
private function __clone() {}
public function __wakeup() {
throw new \Exception("Cannot unserialize singleton");
}
public static function getInstance(): Database {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection(): PDO {
return $this->connection;
}
}
// Использование
$db = Database::getInstance();
$pdo = $db->getConnection();
Важно: Singleton часто критикуют, потому что он создает глобальное состояние. В современных фреймворках (Laravel, Symfony) его заменяют Dependency Injection контейнером. Используйте Singleton только в простых проектах без DI.
2. Factory Method (Фабричный метод) — когда создание объекта сложное
Когда использовать: Вы не знаете заранее, какой именно класс нужно создать, или процесс инициализации объекта требует нескольких шагов. Например, создание разных типов уведомлений (Email, SMS, Push).
interface Notification {
public function send(string $message): void;
}
class EmailNotification implements Notification {
public function send(string $message): void {
echo "Отправка Email: $message\n";
}
}
class SMSNotification implements Notification {
public function send(string $message): void {
echo "Отправка SMS: $message\n";
}
}
abstract class NotificationFactory {
abstract public function createNotification(): Notification;
public function notify(string $message): void {
$notification = $this->createNotification();
$notification->send($message);
}
}
class EmailFactory extends NotificationFactory {
public function createNotification(): Notification {
return new EmailNotification();
}
}
class SMSFactory extends NotificationFactory {
public function createNotification(): Notification {
return new SMSNotification();
}
}
// Использование
$factory = new EmailFactory();
$factory->notify('Ваш заказ подтвержден!');
Польза: Вы отделяете логику создания объекта от его использования. Добавление нового типа уведомления (например, Telegram) не требует изменения существующего кода — просто создаете новый класс фабрики.
3. Strategy (Стратегия) — когда алгоритмы меняются на лету
Когда использовать: У вас есть несколько вариантов поведения (алгоритмов), которые могут переключаться во время выполнения. Типичный пример — расчет стоимости доставки в зависимости от компании (Новая Почта, Укрпочта, DHL).
interface ShippingStrategy {
public function calculateCost(float $weight): float;
}
class NovaPoshtaShipping implements ShippingStrategy {
public function calculateCost(float $weight): float {
return $weight * 15; // 15 грн за кг
}
}
class UkrPoshtaShipping implements ShippingStrategy {
public function calculateCost(float $weight): float {
return $weight * 8 + 20; // 8 грн за кг + фикс 20 грн
}
}
class Order {
private ShippingStrategy $shippingStrategy;
public function __construct(ShippingStrategy $strategy) {
$this->shippingStrategy = $strategy;
}
public function setShippingStrategy(ShippingStrategy $strategy): void {
$this->shippingStrategy = $strategy;
}
public function getShippingCost(float $weight): float {
return $this->shippingStrategy->calculateCost($weight);
}
}
// Использование
$order = new Order(new NovaPoshtaShipping());
echo $order->getShippingCost(2.5); // 37.5
$order->setShippingStrategy(new UkrPoshtaShipping());
echo $order->getShippingCost(2.5); // 40
Совет: Паттерн Strategy часто путают с простым if-else. Если у вас больше 3 вариантов или алгоритмы сложные — Strategy оправдан. Если всего 2 простых условия — оставьте if, не усложняйте.
4. Observer (Наблюдатель) — когда нужно реагировать на события
Когда использовать: Один объект (субъект) меняет состояние, и множество других объектов (наблюдателей) должны автоматически обновиться. Классика — события в фреймворках: после регистрации пользователя нужно отправить письмо, записать в лог, начислить бонус.
interface Observer {
public function update(string $event, $data): void;
}
class User {
private array $observers = [];
public function attach(Observer $observer): void {
$this->observers[] = $observer;
}
public function register(string $email): void {
echo "Пользователь $email зарегистрирован.\n";
$this->notify('user.registered', ['email' => $email]);
}
private function notify(string $event, $data): void {
foreach ($this->observers as $observer) {
$observer->update($event, $data);
}
}
}
class EmailNotifier implements Observer {
public function update(string $event, $data): void {
if ($event === 'user.registered') {
echo "Отправка приветственного письма на {$data['email']