Рано или поздно каждый разработчик сталкивается с необходимостью изучить принципы 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.
Самое интересное — ни одной из этих абстракций бизнес не просил. Мы просто начали защищаться от гипотетического будущего.
Схематично это может выглядеть вот так:
- Команда: продакт-менеджер, менеджер проекта, аналитик, арт-директор, дизайнер и 3 программиста;
В чем проблема такого подхода
На архитектурной диаграмме всё выглядит красиво: гибко и расширяемо. И соответствует 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);
}
}
- Команда: продакт-менеджер, менеджер проекта, аналитик, арт-директор, дизайнер и 3 программиста;
Что мы сознательно не делаем
Мы не добавляем:
- FeedbackDataSourceInterface;
- FeedbackSenderInterface;
- FeedbackStorageAdapter;
- ExternalFeedbackProvider.
Мы не делаем этого, потому что отзывы хранятся в БД, отчет отправляется на имейл, а других требований нет (и скорее всего, не будет). Если когда-нибудь появится внешний сервис, добавить адаптер будет не сложно.
А пока код остается простым, понятным и поддерживаемым. И кроме того, закрывает все потребности бизнеса. Куда бизнес свернет через месяц, неизвестно никому. Поэтому предусмотреть всё заранее в любом случае не получится.
Схематично это может выглядеть так:
Вывод
SRP — это не лицензия на бесконечную декомпозицию и уж точно не приглашение писать архитектуру на случай апокалипсиса. Разделять код имеет смысл только в таких случаях:
- есть разные причины для изменений;
- есть разные акторы;
- части системы могут меняться независимо.
Всё остальное — преждевременная архитектура, а преждевременная архитектура зачастую означает лишний код… Который потом придется поддерживать. Всегда важно критически мыслить. Понимать, где и как нужно заложить возможности расширение, а где это является оверхедом и будет лежать мертвым грузом в проекте.
А поддержка ненужного кода — занятие примерно такое же увлекательное, как чтение логов в пятницу вечером.