Опыт применения SOLID
Использую SOLID как набор практических правил для прикладного кода: сервисов, handler-ов, репозиториев, интеграций и domain-слоя.
SRP — Single Responsibility Principle (принцип единственной ответственности)
У класса должна быть одна причина для изменения.
Корректный подход:
final class Report {}
final class ReportRenderer { public function render(Report $report): string {} }
final class ReportSaver { public function save(string $contents): void {} }Контрпример:
final class ReportService
{
public function generateAndSend(array $data, string $email): void
{
$report = $this->buildReport($data);
$html = $this->renderHtml($report);
file_put_contents('/tmp/report.html', $html);
mail($email, 'Report', $html);
}
}Такой класс одновременно отвечает за сбор данных, рендеринг, сохранение и отправку.
OCP — Open / Closed Principle (принцип открытости / закрытости)
Поведение лучше расширять новыми реализациями, а не переписывать один центральный switch.
Корректный подход:
interface Discount
{
public function apply(int $amount): int;
}
final class NoDiscount implements Discount
{
public function apply(int $amount): int
{
return $amount;
}
}
final class Checkout
{
public function __construct(private Discount $discount) {}
}Контрпример:
final class CheckoutService
{
public function applyDiscount(string $discountType, int $amount): int
{
if ($discountType === 'none') {
return $amount;
}
if ($discountType === 'vip') {
return (int) ($amount * 0.9);
}
if ($discountType === 'employee') {
return (int) ($amount * 0.8);
}
throw new InvalidArgumentException('Unknown discount type.');
}
}При добавлении новой скидки приходится менять существующий класс, а не подключать новую реализацию через общий контракт.
LSP — Liskov Substitution Principle (принцип подстановки Барбары Лисков)
Классы-наследники не должны ломать поведение базового класса. Они не должны ужесточать входные параметры (предусловия), и ослаблять возвращаемые значения (постусловия).
Входные параметры метода подкласса – совпадают или более абстрактные;
возвращаемое значение – совпадает либо подтип возвращаемого значения базового класса;
Метод не должен выбрасывать исключения, которые не свойственны базовому методу;
Инварианты класса должны остаться без изменений;
Подкласс не должен изменять значения приватных полей базового класса.
Контрпример:
class Animal {}
class Dog extends Animal {}
class Service
{
public function handle(Animal $animal): Animal
{
return $animal;
}
}Нарушение по входному параметру: подкласс ужесточает предусловие: вместо Animal требует только Dog.
class BadDogService extends Service
{
public function handle(Dog $animal): Animal
{
return $animal;
}
}Нарушение по возвращаемому значению: подкласс ослабляет постусловие: базовый метод обещает вернуть Dog, а наследник возвращает более общий Animal.
class DogFactory
{
public function make(): Dog
{
return new Dog();
}
}
class BadAnimalFactory extends DogFactory
{
public function make(): Animal
{
return new Animal();
}
}ISP — Interface Segregation Principle (принцип разделения интерфейсов)
Интерфейсы лучше делать узкими и прикладными, чтобы клиент зависел только от нужных ему операций.
Корректный подход:
interface UserReader
{
public function find(int $id): ?array;
}
interface UserWriter
{
public function save(array $user): void;
}Контрпример:
interface UserManagerInterface
{
public function find(int $id): ?array;
public function save(array $user): void;
public function delete(int $id): void;
public function import(array $rows): void;
public function export(): array;
public function sendWelcomeEmail(int $id): void;
}Клиенту, которому нужно только чтение, всё равно приходится зависеть от методов записи, импорта и уведомлений.
DIP — Dependency Inversion Principle (принцип инверсии зависимостей)
Классы верхних уровней не должны зависеть от классов нижних уровней. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Корректный подход:
interface UserRepository
{
public function findByEmail(string $email): ?User;
}
final class RegisterUser
{
public function __construct(private UserRepository $repo) {}
}Контрпример:
final class RegisterUser
{
public function handle(string $email): void
{
$repo = new EloquentUserRepository();
if ($repo->findByEmail($email) !== null) {
return;
}
DB::table('users')->insert(['email' => $email]);
}
}Такой use-case жёстко привязан к конкретной ORM и способу записи в базу.
Что это даёт на практике
- изменения чаще локализуются в одном слое или одной реализации;
- новые варианты поведения проще добавлять через отдельные классы;
- тесты проще писать на уровне интерфейсов и use-case классов;
- границы между
Domain,ApplicationиInfrastructureостаются чище.
DRY, KISS, YAGNI
Дополнительно ориентируюсь на DRY, KISS и YAGNI, но без догматизма: если дублирование или простая реализация дешевле лишней абстракции, не усложняю код заранее.