Всем доброго дня! На связи Валевич Артем, тимлид в AGIMA. Рано или поздно каждый разработчик сталкивается с необходимостью изучить принципы SOLID. Интернет полон теоретических статей с абстрактными примерами — треугольниками, фигурами и системами заказов. В таких примерах всё выглядит красиво. Но когда дело доходит до продакшен-кода, возникает логичный вопрос: как применять эти принципы на практике и не превратить проект в архитектурный космолет? Разбираемся.
Дисклеймер: статья предназначена для новичков, которые только познают все принципы хорошего кода.

Проблема в том, что при написании кода по SOLID многие забывают о других принципах разработки. Например:
KISS;
DRY;
YAGNI.
В реальной разработке эти принципы иногда конфликтуют друг с другом. Например, пытаясь сделать код «гибким» и «расширяемым», можно легко нарушить YAGNI и добавить абстракции, которые не нужны ни сейчас, ни в обозримом будущем.
На практике это выглядит так: простой код на 2–3 класса превращается в непонятный набор из 10+ классов, интерфейсов, фабрик и адаптеров. И самое интересное — бизнес об этом даже не просил.
Проблема №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 — это не лицензия на бесконечную декомпозицию и уж точно не приглашение писать архитектуру на случай апокалипсиса. Разделять код имеет смысл только в таких случаях:
есть разные причины для изменений;
есть разные акторы;
части системы могут меняться независимо.
Всё остальное — преждевременная архитектура, а преждевременная архитектура зачастую означает лишний код… Который потом придется поддерживать. Всегда важно критически мыслить. Понимать, где и как нужно заложить возможности расширение, а где это является оверхедом и будет лежать мертвым грузом в проекте.
А поддержка ненужного кода — занятие примерно такое же увлекательное, как чтение логов в пятницу вечером.
Делитесь в комментариях своими мыслями по поводу SOLID и в частности про SRP. Интересно узнать о вашем опыте.
