Введение: Почему SOLID — это не просто догма, а инструмент
Большинство разработчиков знают, что SOLID — это пять принципов объектно-ориентированного программирования: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation и Dependency Inversion. Однако на практике понимание часто сводится к механическому следованию правилам. В этой статье мы пойдем дальше: разберем продвинутые сценарии применения SOLID в PHP, рассмотрим антипаттерны, которые нарушают принципы, и покажем, как рефакторинг с учетом SOLID делает код гибким и тестируемым.
Материал рассчитан на разработчиков уровня Middle+ и Senior, которые уже пишут на PHP и хотят улучшить архитектуру своих проектов.
1. Принцип единственной ответственности (SRP): когда класс «знает слишком много»
Классическая формулировка: «У класса должна быть только одна причина для изменения». На продвинутом уровне важно понимать, что «причина изменения» — это бизнес-требование. Если ваш класс изменяется из-за изменения правил валидации, формата вывода и логики сохранения — это нарушение SRP.
Пример нарушения (God Object)
class OrderProcessor {
public function process(array $data): void {
// 1. Валидация данных
if (empty($data['user_id']) || empty($data['items'])) {
throw new InvalidArgumentException('Invalid order data');
}
// 2. Подсчет стоимости
$total = 0;
foreach ($data['items'] as $item) {
$total += $item['price'] * $item['quantity'];
}
// 3. Сохранение в БД
$sql = "INSERT INTO orders (user_id, total, status) VALUES (?, ?, 'pending')";
// ... выполнение запроса
// 4. Отправка email
mail($data['user_email'], 'Order confirmation', "Total: $total");
}
}
Рефакторинг с SRP
Разделяем на три класса с четкими зонами ответственности:
class OrderValidator {
public function validate(array $data): void {
if (empty($data['user_id']) || empty($data['items'])) {
throw new InvalidArgumentException('Invalid order data');
}
}
}
class OrderCalculator {
public function calculateTotal(array $items): float {
return array_reduce($items, function ($carry, $item) {
return $carry + $item['price'] * $item['quantity'];
}, 0.0);
}
}
class OrderRepository {
public function save(int $userId, float $total, string $status = 'pending'): int {
// Логика сохранения в БД
return 42; // ID заказа
}
}
class OrderProcessor {
public function __construct(
private OrderValidator $validator,
private OrderCalculator $calculator,
private OrderRepository $repository,
private Mailer $mailer
) {}
public function process(array $data): void {
$this->validator->validate($data);
$total = $this->calculator->calculateTotal($data['items']);
$orderId = $this->repository->save($data['user_id'], $total);
$this->mailer->send($data['user_email'], 'Order #' . $orderId);
}
}
2. Принцип открытости/закрытости (OCP): расширение без модификации
Суть OCP: классы должны быть открыты для расширения, но закрыты для изменения. На практике это означает использование полиморфизма и абстракций вместо условных конструкций.
Нарушение OCP: switch-case на каждый новый тип
class PaymentProcessor {
public function pay(string $type, float $amount): void {
switch ($type) {
case 'credit_card':
// обработка карты
break;
case 'paypal':
// обработка PayPal
break;
// При добавлении нового типа — меняем класс!
}
}
}
Решение через стратегию (Strategy Pattern)
interface PaymentMethod {
public function process(float $amount): bool;
}
class CreditCardPayment implements PaymentMethod {
public function process(float $amount): bool {
// Логика для кредитной карты
return true;
}
}
class PayPalPayment implements PaymentMethod {
public function process(float $amount): bool {
// Логика для PayPal
return true;
}
}
// Новый тип — просто новый класс, без изменения существующего кода
class CryptoPayment implements PaymentMethod {
public function process(float $amount): bool {
// Логика для криптовалюты
return true;
}
}
class PaymentProcessor {
public function __construct(
private PaymentMethod $method
) {}
public function execute(float $amount): bool {
return $this->method->process($amount);
}
}
3. Принцип подстановки Лисков (LSP): контрактное программирование
LSP часто нарушают при наследовании. Формально: «Объекты базового класса должны заменяться объектами производного класса без изменения свойств программы». На PHP это особенно актуально при работе с ковариантностью и контравариантностью.
Типичное нарушение: ослабление предусловий
class Rectangle {
public function setWidth(int $width): void { $this->width = $width; }
public function setHeight(int $height): void { $this->height = $height; }
public function getArea(): int { return $this->width * $this->height; }
}
class Square extends Rectangle {
public function setWidth(int $width): void {
parent::setWidth($width);
parent::setHeight($width); // Нарушение: квадрат меняет высоту!
}
public function setHeight(int $height): void {
parent::setWidth($height);
parent::setHeight($height);
}
}
// Клиентский код сломается:
function printArea(Rectangle $rect): void {
$rect->setWidth(5);
$rect->setHeight(10);
echo $rect->getArea(); // Ожидаем 50, получаем 100 для Square
}
Правильное решение: композиция вместо наследования
interface Shape {
public function getArea(): int;
}
class Rectangle implements Shape {
public function __construct(
private int $width,
private int $height
) {}
public function getArea(): int {
return $this->width * $this->height;
}
}
class Square implements Shape {
public function __construct(
private int $side
) {}
public function getArea(): int {
return $this->side * $this->side;
}
}
4. Принцип разделения интерфейсов (ISP): толстые интерфейсы — зло
ISP гласит: «Клиенты не должны зависеть от методов, которые они не используют». В PHP это особенно важно при работе с типизированными свойствами и readonly-классами.
Пример «толстого» интерфейса
interface Worker {
public function work(): void;
public function eat(): void;
public function sleep(): void;
}
class HumanWorker implements Worker {
public function work(): void { /* работает */ }
public function