У каждой громоздкой системы, которую боится трогать даже её автор, есть своя история про "будущую гибкость". Кто-то добавил абстрактную фабрику на ровном месте, кто-то разнёс монолит на четырнадцать сервисов до появления первого клиента, кто-то собрал универсальный движок правил, которым ни разу не воспользовались. Намерения были добрые, а результат один: система делает простую работу сложным способом, и мелкая доработка тянет за собой неделю согласований. Коварство в том, что сложность почти никогда не приходит одним громким решением. Она копится десятками маленьких "разумных" шагов, каждый из которых по отдельности легко защитить на ревью. Ниже разбор того, откуда сложность берётся, сколько она стоит в сроках и деньгах и почему простые решения чаще выигрывают у вычурных, когда продукту только предстоит расти.

Откуда берётся сложность, которую никто не заказывал

Сцена знакома почти каждому. В задаче на два дня появляется абстрактный базовый класс, за ним фабрика, за фабрикой реестр фабрик, а в конце файла конфигурация, которая выбирает реализацию по имени. Зачем? "Вдруг потом добавим второй платёжный шлюз". Второй шлюз не появляется ни через полгода, ни через два года, но три слоя косвенности уже въелись в код, в тесты и в привычки коллектива.

Механизм роста всегда один. Каждое решение в отдельности выглядит профессионально: интерфейс "правильнее" конкретики, расширяемость "выше", а на контрольный вопрос, какая конкретная задача стоит прямо сейчас, никто не отвечает. Команда проектирует под воображаемое будущее, где клиентов миллионы и требований в тридцать раз больше. Будущее не наступает по расписанию, а код с запасом абстракций обслуживать приходится каждый день. Ловушка называется сверхинженерией, и лечится она не талантом, а дисциплиной: сначала требование, потом боль, потом решение.

Второй канал лишней сложности это мода на инструменты. Услышав про шину событий или микросервисы, команда внедряет их в продукт с тремя сотнями пользователей и тремя разработчиками. Инструмент честно решает чужую проблему чужого масштаба, зато бесплатно приносит новую проблему: время на эксплуатацию, которого никто не закладывал в смету.

Третий канал скромнее на вид, но коварнее: конфигурация, которая незаметно превращается в язык программирования. Сначала в файле лежат три параметра подключения, через год в нём ветвления, подстановки и восемьсот строк, которые никто не решается тронуть, потому что непонятно, что от чего зависит. Получается самодельный язык без отладчика, типов и тестов, то есть случайная сложность в чистом виде. Там же живут самописные движки правил и "универсальные" генераторы отчётов: каждый из них делает работу, которую можно было бы выразить сотней строк прямого кода.

Существенная и случайная сложность по Фредерику Бруксу

Ещё в 1986 году Фредерик Брукс в эссе "No Silver Bullet" разделил сложность программных систем на два вида. Существенная сложность рождается самой задачей: если пользователю нужно тридцать разных функций, программа обязана уметь все тридцать, и никакой модный язык этого требования не отменит. Случайную сложность создают сами инженеры: неудачные инструменты, лишние обёртки, самодельные механизмы там, где хватило бы стандартных. Первую можно только аккуратно нести, вторую можно и нужно устранять.

Там же Брукс сформулировал знаменитый вывод: ни одно отдельно взятое средство, ни в технологии, ни в управлении, само по себе не обещает даже десятикратного улучшения продуктивности, надёжности и простоты за десятилетие. Прошло почти сорок лет, прогноз держится: инструменты стали мощнее на порядок, но ограничитель прежний, способность человека удерживать в голове устройство системы. Эдсгер Дейкстра в заметке 1975 года сказал короче: "Простота является предварительным условием надёжности". Надёжность вырастает не из количества тестов, а из конструкции, которую понятно проверять.

Практический вывод скромен: прежде чем усложнять, честно определи вид сложности. Если требование действительно диктует ветвление логики, это издержки задачи. Если же пишется третья обёртка вокруг одного метода, это издержки вкуса, и их можно срезать без потери пользы.

Глубина модуля как измеритель пользы абстракции

Джон Оустерхаут в книге "A Philosophy of Software Design" 2018 года дал рабочее определение: сложность это всё, что мешает понять систему и изменять её. Его второй вклад, измеритель качества абстракции, называется глубиной модуля. Глубокий модуль прячет много функциональности за коротким интерфейсом: системный вызов read в unix скрывает за четырьмя аргументами буферы, состояние диска и работу драйвера. Мелкий модуль устроен наоборот: его интерфейс по сложности сравним с тем, что он прячет, и польза стремится к нулю.

Разницу видно без лупы, прямо в ревью. Первый фрагмент ниже это типичная попытка "предусмотреть всё", второй делает ту же самую работу.

from abc import ABC, abstractmethod

class PaymentChannel(ABC):
    @abstractmethod
    def pay(self, amount: int) -> bool: ...

class CardChannel(PaymentChannel):
    def pay(self, amount: int) -> bool:
        return amount > 0

class ChannelFactory(ABC):
    @abstractmethod
    def create(self) -> PaymentChannel: ...

class CardChannelFactory(ChannelFactory):
    def create(self) -> PaymentChannel:
        return CardChannel()

Пять классов, двадцать с лишним строк и три уровня косвенности ради одного сравнения с нулём. Второй вариант:

def pay_by_card(amount: int) -> bool:
    return amount > 0

Возражение обычно одно: "потом придётся переписывать". Когда второй шлюз появится, фабрику можно добавить за один час, не ломая вызовы, точка изменения одна. До этого момента первая версия не даёт ничего, кроме лишнего чтения. Плата за иллюзорную гибкость берётся при каждой отладке: чтобы понять, какая реализация реально вызвалась, придётся идти по цепочке из трёх шагов. Абстракция окупается только тогда, когда за ней уже стоят две живые реализации или измеренное требование.

Реальная цена избыточной архитектуры в сроках и деньгах

Цена у сверхинженерии конкретная, и проявляется она в четырёх местах. Онбординг: новичок в системе с пятью слоями обёрток разбирается недели, в системе с прямыми вызовами приносит первую пользу за два-три дня. Ревью: проверка растягивается, ведь рецензент удерживает не только новое поведение, но и все точки расширения, которых касается правка. Отладка: ошибка проходит сквозь интерфейсы, и локализовать её удаётся только пошаговым чтением, оплаченным дважды, при написании и при разборе инцидента. Планирование: оценки расползаются, ведь время на обслуживание гибкости никто не выносит в тикет отдельной строкой.

Тесты множат косвенность: каждый слой получает свои заглушки, каждая заглушка это файл, который стареет вместе с кодом. Число мест, где надо что-то помнить, растёт нелинейно: два лишних слоя удваивают пути чтения, пять превращают понимание в археологию. Через полтора года у команды возникает "авторский инстинкт": уверенно правит только тот, кто помнит историю, и его отпуск становится операционным риском для всего продукта.

Посчитать можно без бухгалтерии. Правка, которая в прямой архитектуре требует часа чтения и часа изменений, на пяти слоях обёрток съедает полдня: по 20-30 минут уходит на каждый слой, хотя меняется одна строка поведения. Если таких правок десять в месяц, команда теряет неделю чистого времени, не считая ошибок от рассеянного внимания. Любая абстракция, которая не решает сегодняшнюю задачу, работает как налог: платёж маленький, но ежедневный, и отменить его задним числом почти невозможно.

Эволюция простых решений вслед за ростом продукта

Против избыточности давно есть два противоядия. Принцип YAGNI из экстремального программирования Кента Бека конца 1990-х годов звучит как приказ: не программируй то, что ещё не понадобилось. Правило трёх, которое Мартин Фаулер связывает с именем Дона Робертса, выражает ту же мысль мягче: первый раз пишем как получается, второй раз помечаем дублирование, третий раз выносим общую часть. Абстракция обязана родиться из повторяющейся боли, а не из предчувствия боли.

Рост зрелой системы тогда выглядит как лестница. Монолит с аккуратными границами модулей честно тянет продукт до реального масштаба. Выносить отдельный сервис стоит только по измеренному поводу: один модуль растёт по нагрузке в десять раз быстрее остальных, у него своя команда, свой график релизов или свои требования к отказоустойчивости. Так вырастали и крупные платформы: сначала цельный монолит, потом сервис за сервисом, каждый под конкретную измеренную боль.

Порядок шагов при эволюции почти всегда одинаков. Сначала внутри монолита проводят границу: модуль получает собственный фасад и запрещает остальным лазить в его таблицы напрямую. Потом границу укрепляют тестами и версиями API. Выносить процесс в отдельный контейнер имеет смысл, когда граница уже спокойна, а нагрузка модуля действительно отличается: например, обработка изображений съедает 70 процентов процессорного времени при 5 процентах кодовой базы. Тогда миграция даёт измеримую выгоду: независимое масштабирование вместо раздувания всего кластера.

Рич Хики в докладе "Simple Made Easy" 2011 года развёл слова, которые многие путают: лёгкое лежит близко под рукой, простое не сплетено в клубок. Лёгкое приятно сразу, простое окупается годами; избыточная архитектура почти всегда лёгкая, но не простая.

Практические приёмы удержания сложности под контролем

Дешёвые привычки работают лучше благородных порывов. Рабочий бюджет абстракций на одну задачу умещается в пять строк:

  1. решать задачу из тикета, а не её гипотетическое будущее, самым прямым способом;
  2. дублировать без страха первые два раза, третья копия это сигнал к рефакторингу;
  3. спрашивать у автора каждого интерфейса, какую сегодняшнюю задачу этот интерфейс решает;
  4. записывать архитектурные решения короткими заметками с причиной, датой и отвергнутыми альтернативами;
  5. вычёркивать мёртвые ветки и неиспользуемые обёртки сразу, без надежды на "авось пригодится".

Пороги поддерживают и автоматы. Статические анализаторы по умолчанию помечают функцию с цикломатической сложностью выше 10 и когнитивной выше 15. Метрика не заменяет вкус, зато переводит спор о вкусе в разговор о конкретном числе. Ещё один приём, явный бюджет сложности на релиз: команда договаривается, что новый модуль не глубже трёх слоёв, и ревью получает ясный критерий вместо расплывчатого "мне не нравится".

Записи о решениях не обязаны быть формальными. Хватает пяти полей: контекст, решение, отвергнутые варианты, последствия, дата. Проверка на вшивость простая: новый разработчик должен понять причину, не поднимая архив переписки, иначе расследование заканчивается фразой "так исторически сложилось".

Когда сложность оправдана измеренными требованиями

Бывают системы, где сложность зарабатывается честно. Платёжный шлюз с десятками миллионов операций в сутки обязан обзавестись репликацией, очередями и правилами деградации. Бортовое ПО с обязательной сертификацией обязано быть избыточно проверяемым. Игровому движку нужен фрейм-бюджет около 16,6 миллисекунды на кадр при 60 кадрах в секунду, и ради него вручную управляют памятью. Общее у таких случаев одно: за каждым усложнением стоит число, доступность 99,99 процента, лимит памяти в 512 килобайт, нагрузка в сотни тысяч запросов в секунду.

Усложнение без числа всегда подозрительно. Если в задаче нет измеренного порога, цифры нагрузки или жёсткого требования среды, а есть только вкус исполнителя, то место такой сложности в бэклоге, а не в коде. Отсюда простая проверка на ревью: попросите автора назвать требование, которое усложнение закрывает. Честный ответ "никакого" экономит команде месяцы.

Простота в проектировании это не бедность средств, а дисциплина расхода. Хороший архитектор ведёт себя как скупец с точным планом: тратит сложность только там, где без неё не работает железо или не сходится экономика, и откладывает решение до момента последней ответственности, когда данных уже достаточно. Пока выбор обратим, дешёвое прямое решение выгоднее дорогого универсального. Прозрачный монолит, который не стыдно рефакторить, обгоняет вычурную конструкцию: его команда быстрее понимает, чинит и добавляет то, что рынок действительно попросил. Системы, доживающие до зрелости, чаще всего начинались со скучного, прямого и оттого живучего решения первой реальной задачи, и это лучшая ставка для следующей.