Принципы SOLID на практике
Применяйте пять принципов SOLID к реальным классам PHP
«Принципы SOLID на практике» — бесплатный урок PHP Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.
Зачем нужен SOLID
SOLID — это пять принципов проектирования, которые делают объектно-ориентированный PHP гибким, пригодным для тестирования и устойчивым к деградации. Это не правила, которым нужно слепо следовать, а эвристики, уменьшающие связанность и проясняющие зоны ответственности. В этом уроке Вы примените каждый принцип к конкретным классам PHP, которые действительно можно отправить в production.
- Single Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
Единственная ответственность
У класса должна быть одна причина для изменения. У класса, который формирует отчёт, преобразует его в HTML и отправляет по электронной почте, есть три причины для изменения. Разделите сохранение данных, форматирование и доставку.
Ниже Invoice отвечает только за моделирование данных; формирование представления и сохранение выполняются в других местах.
<?php
final class Invoice {
public function __construct(
public readonly string $number,
public readonly int $cents
) {}
public function total(): float { return $this->cents / 100; }
}
final class InvoiceRenderer {
public function toText(Invoice $i): string {
return sprintf('Invoice %s: $%.2f', $i->number, $i->total());
}
}
$i = new Invoice('INV-1', 12500);
echo (new InvoiceRenderer())->toText($i), PHP_EOL;
SRP: как заметить проблему
Классическое нарушение SRP — это класс, в названии которого есть и, или Manager, который делает всё подряд. Обращайте внимание на методы, изменяющиеся по не связанным друг с другом бизнес-причинам. Если изменение налогового правила и изменение макета PDF затрагивают один и тот же класс, у него слишком много обязанностей.
Принцип открытости/закрытости
Программные сущности должны быть открыты для расширения и закрыты для изменения. Добавление нового типа скидки не должно заставлять Вас редактировать огромный switch. Используйте полиморфизм: каждое правило должно быть отдельным классом, реализующим общий интерфейс.
<?php
interface Discount { public function apply(float $total): float; }
final class PercentOff implements Discount {
public function __construct(private float $pct) {}
public function apply(float $t): float { return $t * (1 - $this->pct / 100); }
}
final class FlatOff implements Discount {
public function __construct(private float $amount) {}
public function apply(float $t): float { return max(0, $t - $this->amount); }
}
function checkout(float $total, Discount ...$discounts): float {
foreach ($discounts as $d) { $total = $d->apply($total); }
return $total;
}
echo checkout(100, new PercentOff(10), new FlatOff(5)), PHP_EOL; // 85
Подстановка Лисков
Подтипы должны быть пригодны для использования везде, где ожидается их базовый тип, не удивляя вызывающий код. Известный пример: Square extends Rectangle нарушает LSP, поскольку изменение ширины не должно незаметно изменять высоту. Предпочтительно моделировать их как отдельные типы за общим интерфейсом Shape.
<?php
interface Shape { public function area(): float; }
final class Rectangle implements Shape {
public function __construct(private float $w, private float $h) {}
public function area(): float { return $this->w * $this->h; }
}
final class Square implements Shape {
public function __construct(private float $side) {}
public function area(): float { return $this->side ** 2; }
}
$shapes = [new Rectangle(2, 3), new Square(4)];
foreach ($shapes as $s) { echo $s->area(), PHP_EOL; }
LSP и контракты
LSP также ограничивает контракты методов. Подтип может ослаблять предусловия и усиливать постусловия, но не наоборот. Создание нового типа исключения, о котором базовый тип ничего не объявлял, или возврат null там, где базовый тип гарантирует объект, нарушает принцип подстановки. Ковариантные типы возвращаемых значений и контравариантные типы параметров в PHP обеспечивают часть этого требования на уровне типов.
Разделение интерфейса
Клиенты не должны зависеть от методов, которые они не используют. Большой интерфейс Worker с методами work() и eat() заставляет RobotWorker создавать заглушку для eat(). Разделяйте интерфейсы по узким ролям, чтобы каждая реализация брала на себя только те обязанности, которые действительно выполняет.
<?php
interface Workable { public function work(): string; }
interface Feedable { public function eat(): string; }
final class Human implements Workable, Feedable {
public function work(): string { return 'coding'; }
public function eat(): string { return 'lunch'; }
}
final class Robot implements Workable {
public function work(): string { return 'welding'; }
}
foreach ([new Human(), new Robot()] as $w) { echo $w->work(), PHP_EOL; }
Инверсия зависимостей
Высокоуровневая логика должна зависеть от абстракций, а не от конкретных деталей. Внедряйте интерфейс, а не жёстко заданный класс. Это позволяет в тестах заменить настоящий почтовый сервис имитацией, а в production — использовать другого поставщика, не изменяя код потребителя.
<?php
interface Mailer { public function send(string $to, string $body): void; }
final class SmtpMailer implements Mailer {
public function send(string $to, string $body): void {
echo "SMTP -> $to: $body" . PHP_EOL;
}
}
final class SignupService {
public function __construct(private Mailer $mailer) {}
public function register(string $email): void {
$this->mailer->send($email, 'Welcome!');
}
}
(new SignupService(new SmtpMailer()))->register('a@b.com');
DIP и контейнер
Инверсия зависимостей — это принцип, а внедрение зависимостей — один из способов его реализовать. Контейнер DI (PSR-11) связывает конкретный SmtpMailer с абстракцией Mailer в корне композиции. Потребитель никогда не использует new SmtpMailer(), поэтому направление зависимости исходного кода указывает на абстракцию, а не на деталь.
<?php
// Composition root wiring (pseudo-container)
$bindings = [
Mailer::class => fn() => new SmtpMailer(),
];
$resolve = fn(string $id) => $bindings[$id]();
$service = new SignupService($resolve(Mailer::class));
$service->register('user@example.com');
SOLID вместе
Эти принципы усиливают друг друга. ISP сохраняет интерфейсы небольшими, поэтому внедрение зависимостей по DIP остаётся целенаправленным; OCP опирается на полиморфизм, безопасность которого обеспечивает LSP; SRP даёт небольшие классы, благодаря которым всё это становится возможным. Стремитесь к высокой связности внутри компонентов и слабой связанности между ними.
- Не создавайте избыточную абстракцию для класса, используемого один раз.
- Вводите интерфейс, когда появляется вторая реализация или тестовая замена.
Прагматизм
SOLID — это средство, а не цель. Преждевременный интерфейс с одной реализацией добавляет косвенный слой без пользы («спекулятивное обобщение»). Применяйте эти принципы, когда изменения действительно появляются или явно неизбежны. Рефакторинг в сторону SOLID обходится недорого, если у Вас уже есть тесты; слепое движение к SOLID с первого дня — пустая трата времени.
Быстрая проверка
Какой принцип нарушает проблема наследования Square/Rectangle?
Повторение
Вы применили все пять принципов SOLID к реальному PHP:
- SRP: одна причина для изменения у каждого класса.
- OCP: расширение с помощью новых полиморфных классов, а не редактирование оператора выбора.
- LSP: подтипы соблюдают контракт базового типа.
- ISP: небольшие интерфейсы ролей вместо больших.
- DIP: зависимость от абстракций и внедрение деталей в корне композиции.
Используйте эти принципы для руководства рефакторингом, а не ради формальности.
Часто задаваемые вопросы
Урок «Принципы SOLID на практике» бесплатный?
Да — полный текст урока «Принципы SOLID на практике» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.
Чему я научусь в уроке «Принципы SOLID на практике»?
Применяйте пять принципов SOLID к реальным классам PHP Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать PHP Academy?
Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «Принципы SOLID на практике»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке PHP Academy?
Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Принципы SOLID на практике
- Порождающие шаблоны: Factory, Builder, Singleton
- Структурные шаблоны: Adapter, Decorator, Facade
- Поведенческие шаблоны: Strategy, Observer, Command