Занимаясь проектированием систем ПО, идите самым простым путём из возможных.

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

Многие инженеры, продумывая дизайн системы, представляют себе некий её «идеал»: что-то стройное, практически бесконечно масштабируемое, удобно распространяемое и так далее. Я же считаю, что это абсолютно ошибочный подход к проектированию ПО. Напротив, нужно вложить всё это время в глубокий анализ имеющейся системы, а затем реализовать простейшее рабочее решение.

Простота может казаться посредственной

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

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

На языке ПО это означает, что продуманный дизайн выглядит посредственно. По нему не скажешь, что там происходит нечто серьёзное. Понять это можно, когда тебе начинают приходить мысли вроде «А я и не думал, что всё было так просто» или «Класс, а ведь ничего сложного и не требуется».

Примером крутого дизайна является Unicorn, так как все важнейшие гарантии на уровне веб-сервера — изоляция запросов, горизонтальное масштабирование, восстановление после сбоя — обеспечивает за счёт примитивов Unix.1 Промышленный стандарт Rails REST API тоже можно отнести к примерам грамотного дизайна, так как он даёт тебе всё необходимое для приложения CRUD максимально скучным образом. Не думаю, что эти примеры являются примером внешне впечатляющего ПО. Но они впечатляют своим дизайном, потому что реализуют простейшие рабочие решения.

И вам следует поступать так же! Предположим, вы работаете над приложением Go, в которое хотите добавить некий механизм ограничения частоты запросов. Какое решение здесь будет простейшим? Первой мыслью может быть добавление постоянного хранилища (например, Redis) для отслеживания числа запросов каждого пользователя с помощью алгоритма «дырявое ведро». И это сработает. Но нужен ли вам целый дополнительный элемент инфраструктуры?

А что, если вместо этого сохранять данные о количестве запросов в памяти? Да, вы будете терять часть информации об их числе при перезапуске приложения, но разве это важно? Да и вообще, вы не задумывались, вдруг ваш периферийный прокси2 уже поддерживает возможность ограничения частоты запросов? Может, вам достаточно написать пару строк в файле конфигурации, а не реализовывать эту функцию целиком?

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

Причём вы можете создать по такому принципу целое приложение с нуля. Начните с самого простого и затем добавляйте что-либо, только когда возникнет необходимость. Звучит глупо, но работает. Можете рассматривать это как возведение во главу всего принципа YAGNI: выше принципа единственной ответственности, выше идеи выбора лучшего инструмента для задачи и выше «хорошего дизайна».

Чем грозит использование простейших решений?

Естественно, следование такому подходу может вызывать серьёзные вопросы, конкретно три. Первый заключается в том, что без понимания, какие требования возникнут в будущем, вы получите негибкую систему, больше похожую на большой ком грязи (big ball of mud). Второй вопрос в том, как понимать «простейшее». А третий в том, что вам нужно строить системы, которые будут масштабироваться, а не просто работать конкретно сейчас. Давайте разберём все эти потенциальные возражения по порядку.

Большой ком грязи

Для некоторых разработчиков фраза «используй простейшее решение» звучит так, будто я прошу их перестать заниматься инжинирингом. Если простейшее решение обычно представляет наспех состряпанный клудж, значит ли это, что мой совет неизбежно приведёт к полному хаосу? Мы все видели кодовые базы, где поверх одной кучи хаков наворочена другая куча — и они явно не являются эталоном удачного дизайна.

Но действительно ли эти хаки простые? Я лично так не думаю. Проблема с использованием хаков или реализацией клуджей как раз в отсутствии простоты. Всё это добавляет в базу кода сложность, внося очередной элемент, который нужно всегда иметь в виду. Разве что хаки проще придумать. Сообразить должное исправление трудно, потому что для этого нужно понимать всю кодовую базу или её значительные части. В действительности же полноценное исправление всегда намного проще хака.

Непросто реализовать простейшее рабочее решение. Когда перед вами возникает проблема, первая пара идей, которая идут на ум, вряд ли окажутся простейшими. Для того чтобы найти самое простое, потребуется рассмотреть множество разных подходов. Иными словами, потребуется тот самый инжиниринг.

Что значит «простой»?

Разработчики часто расходятся в понимании того, что значит простой код. Если «простейший» уже подразумевает «с хорошей структурой», не будет ли тавтологией говорить «используй простейшее рабочее решение». Иными словами, можно ли сказать, что Unicorn проще Puma?3 Окажется ли подсчёт количества запросов в памяти проще, чем использование Redis? Вот вам грубое, чисто интуитивное определение простоты:4

  1. В простых системах меньше «подвижных частей»: меньше элементов, о которых необходимо думать во время работы.

  2. Простые системы имеют меньше внутренних связей. Они состоят из компонентов с чёткими и простыми интерфейсами.

Процессы Unix проще, чем потоки (а значит, Unicorn проще Puma), потому что они менее связаны — то есть не используют общую память. И я вижу в этом реальный смысл. Но я не думаю, что такой принцип определения простоты подойдёт всегда и везде.

А что насчёт подсчёта запросов в памяти в сравнении с использованием Redis? С одной стороны, использовать оперативную память проще, так как не придётся продумывать все элементы, связанные с запуском отдельного сервиса с постоянной памятью. С другой же, Redis проще, так как обеспечивает более простой механизм ограничения частоты запросов — не нужно беспокоиться о случаях, когда один сервер думает, что частота пользовательских запросов ограничена, а другой считает, что нет.

Если я не уверен, что именно в конкретном случае «проще», то ориентируюсь на контрольное утверждение: простые системы стабильны. Если вы сравниваете два состояния программной системы, и одно из них потребует больше регулярной работы при условии отсутствия изменений в требованиях, тогда второе будет проще. Redis необходимо развёртывать и обслуживать — этот механизм может иметь собственные проблемы, требует отдельного мониторинга, его нужно каждый раз развёртывать в любой новой среде, где оказывается сервис, и так далее. С учётом всего этого реализовать отслеживание запросов в памяти будет проще, чем использовать Redis.5

Почему вас может не интересовать масштабирование?

Часть разработчиков сейчас точно возмутились с мыслями «Но такой механизм с подсчётом запросов в памяти не будет масштабироваться…». То есть простейшее рабочее решение определённо не обеспечит возможность масштабирования системы в веб-среде. Мы получим систему, которая прекрасно работает в текущем масштабе. Можно ли назвать это безответственной разработкой?

Нет. На мой взгляд, главный грех в сфере SaaS-разработки — это одержимость масштабом. Я неоднократно видел страдания, возникающие из-за оверинжиниринга систем, когда их готовили работать в масштабах, во много раз превышающих текущий.

Основная причина так не делать в том, что это не работает. Из своего опыта могу сказать, что для любой нетривиальной кодовой базы невозможно предугадать, как она поведёт себя при многократном увеличении трафика. Ведь вы заранее не знаете, где окажутся узкие места системы. В лучшем случае вы можете постараться подготовиться к 2-х или 5-кратному росту трафика и затем быть готовыми к решению возникающих проблем.

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

Выводы

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

Если говорить в общем, то есть два пути развития ПО. Первый — это прогнозировать, как будут выглядеть требования где-нибудь через шесть месяцев или год, и стараться спроектировать максимально подходящую под это видение систему. Второй — это создавать оптимальную систему под те требования, которые есть сейчас. Иначе говоря, идти путём простейшего рабочего решения.

Дополнение: эта статья разожгла активную дискуссию на Hacker News.

В одной интересной ветке комментариев участник пишет, что простота архитектуры в масштабах неважна, так как сложность «исследования пространства состояний в реализации»* (думаю, речь примерно о том, что я описывал здесь) преобладает над любой другой сложностью. И здесь я не согласен. Чем больше усложняется взаимодействие между функциями программы, тем важнее становится простота архитектуры, так как «бюджет сложности» оказывается практически исчерпан.

Также хочу отдать должное Уорду Каннингему и Кенту Беку как истинным авторам этого утверждения — я искренне считал, что придумал его сам, но наверняка просто запомнил. Спасибо пользователю HN ternaryoperator за то, что указал на этот факт.

*В оригинале: state space exploration in implementation.

Сноски

1. Это всего‑навсего сокеты и разветвлённые процессы Unix! Обожаю Unicorn.

2. У каждой технологической компании есть некий пограничный прокси.

3. Мне нравится Puma, это хороший веб‑сервер. Определённо есть случаи, когда он будет предпочтительнее Unicorn (хотя лично я в таких случаях хорошенько подумал бы над поиском альтернативы Ruby).

4. Здесь я опираюсь на известное выступление Рича Хикки »Simple Made Easy». Я не во всём с ним согласен (уверен, что хорошее знание предмета на практике способствует простоте), но посмотреть точно советую.

5. Естественно, если предполагается серьёзное горизонтальное масштабирование системы, то хранение в памяти информации о запросах не сработает, и этот механизм нужно будет заменить чем-нибудь вроде Redis. Но мой опыт говорит, что сервис на Go может хорошо масштабироваться без необходимости горизонтального расширения более, чем на несколько реплик.  

Комментарии (0)


  1. T700
    26.09.2025 13:48

    Отличная статья. Проект всегда надо стартовать с наиболее простой архитектурой. И чем она проще, тем сильнее требования к качеству этой простоты. Понятный и читаемый код, в едином стиле. Ибо любая система, которую долго делают, неизбежно усложнится, под воздействием огромого числа доделак и дополнений. И надо уметь держать баланс и делать постоянно системным подходом, с сохранением порядка.

    В наше время: фреймворки всегда и везде. Ибо так легко менять работника.