Урок 01

SOLID: SRP — единственная ответственность

Что такое SOLID

SOLID — 5 принципов ООП, сформулированных Робертом Мартином. Помогают писать поддерживаемый код.

Расшифровка:

  • S — Single Responsibility Principle.
  • O — Open/Closed Principle.
  • L — Liskov Substitution Principle.
  • I — Interface Segregation Principle.
  • D — Dependency Inversion Principle.

SRP

Single Responsibility Principle — у класса должна быть одна причина для изменения.

Проще: класс делает одно дело.

Проблема

Класс делает всё:

class User
{
    public function __construct(
        private string $name,
        private string $email,
    ) {}

    public function save(): void
    {
        // подключение к БД, INSERT/UPDATE
    }

    public function sendWelcomeEmail(): void
    {
        // отправка email
    }

    public function toJson(): string
    {
        return json_encode(['name' => $this->name]);
    }

    public function validate(): bool
    {
        return filter_var($this->email, FILTER_VALIDATE_EMAIL) !== false;
    }
}

5 причин меняться:

  • Изменение модели данных.
  • Изменение БД.
  • Изменение шаблона письма.
  • Изменение формата JSON.
  • Изменение правил валидации.

Рефакторинг

Разделяем:

final readonly class User
{
    public function __construct(
        public string $name,
        public string $email,
    ) {}
}

final class UserValidator
{
    public function validate(User $user): bool
    {
        return filter_var($user->email, FILTER_VALIDATE_EMAIL) !== false;
    }
}

final class UserRepository
{
    public function save(User $user): void
    {
        // INSERT/UPDATE
    }
}

final class UserMailer
{
    public function sendWelcome(User $user): void
    {
        // отправка
    }
}

final class UserJsonSerializer
{
    public function toJson(User $user): string
    {
        return json_encode([
            'name' => $user->name,
            'email' => $user->email,
        ]);
    }
}

Каждый класс — одна задача.

Как определить

Формулируй ответственность:

«Этот класс отвечает за...»

Если союз «и» — вероятно, больше одной ответственности.

Плохо: «User отвечает за хранение данных и отправку email и валидацию».

Хорошо: «UserValidator отвечает за валидацию».

Не путай с размером

SRP ≠ маленький класс.

Класс может быть большим, если у него одна ответственность.

Пример: HtmlSanitizer с 500 строк — одна ответственность.

И маленький класс с двумя задачами — нарушает SRP.

Уровни ответственности

Три уровня:

  1. Класс — один класс, одна задача.
  2. Метод — один метод, одно действие.
  3. Модуль — один модуль, одна область.

SRP работает на всех.

Признаки нарушения

1. God Object — класс знает всё:

class Application
{
    public function handleRequest(): void { }
    public function route(): void { }
    public function render(): void { }
    public function sendEmail(): void { }
    public function log(): void { }
    // ... 1000 строк
}

2. Много причин для изменения.

3. Названия с «And» или «Manager»:

class UserAndOrderManager { }
class DataProcessorAndNotifier { }

4. Класс с 20+ методами разной тематики.

Практика

Плохо:

class Report
{
    public function generate(): array
    {
        return $this->fetchData();
    }

    private function fetchData(): array
    {
        // запрос к БД
        return [];
    }

    public function toHtml(): string
    {
        return '<table>...</table>';
    }

    public function toPdf(): string
    {
        return 'PDF content';
    }

    public function sendByEmail(string $to): void
    {
        // отправка
    }
}

Хорошо:

final readonly class ReportData
{
    public function __construct(
        public array $rows,
        public string $title,
    ) {}
}

final class ReportDataProvider
{
    public function getData(): ReportData
    {
        return new ReportData([], 'Sales Report');
    }
}

interface ReportFormatter
{
    public function format(ReportData $data): string;
}

final class HtmlFormatter implements ReportFormatter
{
    public function format(ReportData $data): string
    {
        return "<h1>{$data->title}</h1>";
    }
}

final class PdfFormatter implements ReportFormatter
{
    public function format(ReportData $data): string
    {
        return 'PDF: ' . $data->title;
    }
}

final class ReportMailer
{
    public function send(ReportData $data, string $to, ReportFormatter $formatter): void
    {
        $body = $formatter->format($data);
        // отправка
    }
}

Каждый класс — своя задача.

Границы

Слишком мелко — тоже плохо:

class UserNameGetter { }
class UserEmailGetter { }
class UserAgeGetter { }
class UserNameSetter { }
// 20 классов на одного User

Атомизация ради атомизации — вредна.

Правило: разделяй, если меняются по разным причинам.

Как применять на практике

  1. Начинай с одного класса. Не атомизируй заранее.
  2. Когда появляется вторая причина для изменения — разделяй.
  3. Группируй по «изменяющему агенту»:
    • БД меняется — Database-классы.
    • UI меняется — UI-классы.
    • Бизнес-правила — Domain.

Практика: User

Плохо:

class User
{
    public function register(array $data): void
    {
        // валидация
        if (empty($data['email'])) throw new Exception();

        // хеширование
        $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);

        // сохранение
        $pdo = new PDO('...');
        $stmt = $pdo->prepare('INSERT INTO users ...');
        $stmt->execute($data);

        // письмо
        mail($data['email'], 'Welcome', '...');
    }
}

Хорошо:

final readonly class RegisterUserDto
{
    public function __construct(
        public string $name,
        public string $email,
        public string $password,
    ) {}
}

final class RegisterUserValidator
{
    public function validate(RegisterUserDto $dto): void
    {
        if (empty($dto->email)) {
            throw new InvalidArgumentException('Email обязателен');
        }
    }
}

final class UserFactory
{
    public function create(RegisterUserDto $dto): User
    {
        return new User(
            name: $dto->name,
            email: $dto->email,
            passwordHash: password_hash($dto->password, PASSWORD_DEFAULT),
        );
    }
}

final class UserRepository
{
    public function save(User $user): void
    {
        // SQL
    }
}

final class WelcomeMailer
{
    public function send(User $user): void
    {
        // отправка
    }
}

final class RegisterUserHandler
{
    public function __construct(
        private RegisterUserValidator $validator,
        private UserFactory $factory,
        private UserRepository $repository,
        private WelcomeMailer $mailer,
    ) {}

    public function handle(RegisterUserDto $dto): User
    {
        $this->validator->validate($dto);
        $user = $this->factory->create($dto);
        $this->repository->save($user);
        $this->mailer->send($user);

        return $user;
    }
}

RegisterUserHandler — координатор, но у него одна ответственность — оркестрация процесса регистрации.

Итог

  • SRP — одна причина для изменения.
  • Класс — одна задача.
  • «И» в описании — признак нарушения.
  • Не атомизируй заранее.
  • Разделяй по агентам изменений.
  • SRP ≠ маленький класс.

Дальше — SOLID: OCP — открытость/закрытость.