SOLID в реальном мире: SRP без архитектурных космолетов
SRP не требует создавать отдельный класс для каждого действия. Принцип единственной ответственности предлагает разделять код по причинам изменения и интересам разных акторов. Абстракции нужны только там, где они уменьшают связанность и отвечают реальным требованиям.
Коротко
| Ошибка | Практичный подход |
|---|---|
| Проектирование на гипотетическое будущее | Добавлять абстракции по подтверждённым требованиям |
| Интерфейс для каждого класса | Выделять контракт только при нескольких реализациях или внешней границе |
| Один сервис для всех сценариев | Разделять зоны изменений разных акторов |
| Слепое следование SRP | Сопоставлять SRP с KISS, DRY и YAGNI |
Проверка решения по SRP
| Вопрос | Если ответ «да» |
|---|---|
| Компонент меняется по запросу разных ролей? | Разделите ответственности |
| Есть несколько реальных реализаций? | Рассмотрите интерфейс |
| Абстракция упрощает тестирование границы? | Зафиксируйте контракт |
| Изменение пока только предполагается? | Не усложняйте код заранее |
Проблема №1. Нарушение YAGNI
Одна из самых частых проблем, которые я вижу на практике — нарушение принципа YAGNI. Разработчики начинают проектировать систему так, будто заранее знают, как будет развиваться бизнес. Но в реальности этого не знает никто. Даже сам бизнес.
В результате появляются:
- абстракции «на будущее»;
- интерфейсы без реальных альтернативных реализаций;
- архитектурные конструкции, которые никогда не используются.
Код становится сложнее, но не гибче.
Проблема №2. Охота за интерфейсами
Другая распространенная проблема — создание десятков интерфейсов в погоне за принципом Interface Segregation. Сам принцип отличный. Но иногда он превращается в соревнование:
кто создаст больше интерфейсов с одним методом.
Формально всё красиво. Практически — оверхед.
И это только часть проблем, остальные разберем в отдельных статьях.
Что на самом деле имели в виду авторы SOLID
Многие разработчики в целом неправильно понимают, какой смысл закладывали создатели SOLID. Возьмем самый известный принцип — SRP. Чаще всего его формулируют так:
У модуля должна быть одна и только одна причина для изменения.
Но сам Роберт Мартин, создатель методологии ООП, сформулировал его немного точнее:
Модуль должен быть ответственен перед одним и только одним актором.
И это важная деталь. SRP — это не про количество методов в классе и не про то, чтобы каждый класс делал «что-то одно». SRP — это про то, кто инициирует изменения.
Кейс: система отзывов
Рассмотрим простой продакшен-кейс. Есть продукт с несколькими каналами:
- Сайт.
- Мобильное приложение.
- PWA.
Нужно реализовать систему отзывов:
- Пользователь может оставить отзыв.
- Отзывы можно посмотреть в админке.
- Раз в неделю формируется отчет в XLSX и отправляется на почту.
На первый взгляд задача довольно простая. И вот здесь момент, когда многие разработчики достают из кобуры SOLID и начинают стрелять абстракциями во всё, что движется. Иногда даже в то, что не движется.
Как появляется архитектурный космолет
Очень часто разработчик начинает рассуждать примерно так:
- А вдруг отзывы будут храниться не только в БД?
- А вдруг их будут получать из внешнего сервиса?
- А вдруг отчет будут отправлять не только на почту?
- А вдруг появится еще пять каналов?
И тут начинается инженерная фантастика. В коде появляются примерно такие вещи:
interface FeedbackDataSource
{
public function getFeedback(): array;
}
class DatabaseFeedbackSource implements FeedbackDataSource
{
public function getFeedback(): array
{
// получение из БД
}
}
interface FeedbackSender
{
public function send(Report $report): void;
}
class EmailFeedbackSender implements FeedbackSender
{
public function send(Report $report): void
{
// отправка письма
}
}
interface FeedbackChannel
{
public function collect(FeedbackDTO $dto): void;
}Потом добавляются реализации:
- WebsiteFeedbackChannel;
- MobileAppFeedbackChannel;
- PwaFeedbackChannel;
А затем появляются:
- FeedbackRepositoryInterface;
- FeedbackExporterInterface;
- FeedbackProviderFactory;
- AbstractFeedbackExporter.
И где-то на этом этапе простая фича превращается в архитектурный симулятор NASA. Самое интересное — ни одной из этих абстракций бизнес не просил. Мы просто начали защищаться от гипотетического будущего.
Схематично это может выглядеть вот так:
В чем проблема такого подхода
На архитектурной диаграмме всё выглядит красиво: гибко и расширяемо. И соответствует SOLID. Но в реальности мы получаем:
- десятки классов;
- сложную навигацию по коду;
- абстракции без реальных реализаций;
- лишнюю когнитивную нагрузку для команды.
И самое главное: мы оптимизировали архитектуру под будущее, которое может никогда не наступить. Это прямое нарушение YAGNI.
Но и игнорировать SRP нельзя
Есть и другая крайность. Можно сказать: «Да ну этот SOLID. Сделаем один FeedbackService на 800 строк и поедем дальше». Это тоже плохая идея.
Чтобы принять разумное решение, нужно задать главный вопрос SRP:
кто является актором изменений?Точно не интерфейсы. Не абстракции. И не «вдруг когда-нибудь».
Кто акторы в нашем кейсе
В реальном продукте ситуация может выглядеть примерно так. Продакт сайта хочет показывать форму после покупки и собирать имейлы. Продакт мобильного приложения хочет показывать NPS после третьего запуска, а имейлы ему не нужны. Продакт PWA хочет максимально короткую форму Здесь важно заметить одну вещь. Каналы могут иметь разные правила сбора отзывов. А значит, изменения могут происходить независимо:
- правила сайта могут поменяться;
- правила мобильного приложения — нет;
- PWA может вообще отключить сбор отзывов.
Вот здесь появляются разные акторы, а значит, и разные причины для изменений. И это уже сигнал для применения SRP.
Как может выглядеть разумная архитектура
Без космолета, но и без God Object.
class FeedbackService
{
// ответственность: сохранение отзывов
public function __construct(private FeedbackRepository $repository) {}
public function submit(Feedback $feedback): void
{
$this->repository->save($feedback);
}
}
class FeedbackRepository
{
// ответственность: хранение отзывов
// источник данных один — база данных
public function save(Feedback $feedback): void
{
// запись в БД
}
public function getAll(): array
{
// получение отзывов
}
}
class FeedbackReportService
{
// ответственность: формирование и отправка отчёта
public function __construct(private FeedbackRepository $repository,
private ReportMailer $mailer) {}
public function sendWeeklyReport(): void
{
$feedback = $this->repository->getAll();
$report = FeedbackXlsxReport::generate($feedback);
$this->mailer->send($report);
}
}Что мы сознательно не делаем
Мы не добавляем:
- FeedbackDataSourceInterface;
- FeedbackSenderInterface;
- FeedbackStorageAdapter;
- ExternalFeedbackProvider.
Мы не делаем этого, потому что отзывы хранятся в БД, отчет отправляется на имейл, а других требований нет (и скорее всего, не будет). Если когда-нибудь появится внешний сервис, добавить адаптер будет не сложно.
А пока код остается простым, понятным и поддерживаемым. И кроме того, закрывает все потребности бизнеса. Куда бизнес свернет через месяц, неизвестно никому. Поэтому предусмотреть всё заранее в любом случае не получится.
Схематично это может выглядеть так:
Вывод
SRP — это не лицензия на бесконечную декомпозицию и уж точно не приглашение писать архитектуру на случай апокалипсиса. Разделять код имеет смысл только в таких случаях:
- есть разные причины для изменений;
- есть разные акторы;
- части системы могут меняться независимо.
Всё остальное — преждевременная архитектура, а преждевременная архитектура зачастую означает лишний код… Который потом придется поддерживать. Всегда важно критически мыслить. Понимать, где и как нужно заложить возможности расширение, а где это является оверхедом и будет лежать мертвым грузом в проекте.
А поддержка ненужного кода — занятие примерно такое же увлекательное, как чтение логов в пятницу вечером.


