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.
Уровни ответственности
Три уровня:
- Класс — один класс, одна задача.
- Метод — один метод, одно действие.
- Модуль — один модуль, одна область.
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
Атомизация ради атомизации — вредна.
Правило: разделяй, если меняются по разным причинам.
Как применять на практике
- Начинай с одного класса. Не атомизируй заранее.
- Когда появляется вторая причина для изменения — разделяй.
- Группируй по «изменяющему агенту»:
- БД меняется — 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 — открытость/закрытость.