Всем доброго дня! На связи Валевич Артем, тимлид в 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. Интересно узнать о вашем опыте. 

Что еще почитать