Явный дизайн в разработке программ. Предварительные выводы

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

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

Начав работу, я понял, что уместить всё в одну серию разом у меня не получится — слишком много материала. Становилось страшно бросить на половине пути и так и не выпустить ни одного поста.

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

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

Анализ написанного приложения

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

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

Однако, мы всё же можем сделать некоторые выводы о пользе и проблемах идей, которые успели опробовать в предыдущих постах.

Деление по фичам кажется полезным

Разделять функциональность приложения «по фичам» кажется полезным даже в небольших приложениях.

Такое разделение находит «кластеры» связанной по смыслу функциональности и определяет границы между ними. Это помогает дробить крупные задачи, управлять сложностью кода и погружать читателя в контекст приложения постепенно и дозировано.

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

Контроль зацепления даёт шанс вовремя отреагировать, если разные части кода начинают знать об устройстве друг друга слишком много.

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

Везде и повсюду трейдоффы

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

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

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

В общем, серебряной пули нет; в любом случае придётся много думать

…Поэтому просто «взять один принцип и использовать его везде» не получится. Выделять доменную модель в CRUD-приложении вряд ли оправданно, а разделять на слои код простой формочки в интернет-магазине, вероятно, избыточно.

«Почти, но не совсем»

JavaScript (и вследствие TypeScript) — это мультипарадигменный язык. Он позволяет использовать разные инструменты для решения задач, но как бы «на полшишечки».

Для настоящего™ функционального программирования в JS не хватает (по крайней мере пока: 1, 2) нативного паттерн-матчинга и частичного применения функций. Для нормального ООП — не хватает настоящих интерфейсов и компайл-тайм инъекции зависимостей.

В итоге использовать как бы можно и то, и другое; но ни то, ни другое не будет удобно

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

Постоянная борьба с энтропией

Из мультипарадигменности также растут проблемы с «нестадартными решениями».

Например, я не уверен, что идея «явной композиции» будет близка сколько-нибудь значительному количеству разработчиков. Такой способ компоновать модули выглядит странно, непривычно и «нестандартно».

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

Это не хорошо и не плохо, так работает энтропия. А вот бороться с ней и плыть ли против мейнстрима, амбассадоря какие-либо идеи — 🤷

Излишний пуризм стоит денег

В некоторых прошлых постах я акцентировал внимание на том, как можно «срезать углы» при использовании того или иного концепта.

У многих показанных решений можно выделить «степени градации» их строгости. Чем строже реализация идеи, тем ближе она к «эталону»* и тем она более затратна. И наоборот, чем вольнее реализация, тем проще её использовать, но тем дальше она от оригинальной задумки.

Во многих случаях «вольность в реализации» слабо влияет на пользу, но может снизить её издержки. (Как, например, прямые импорты хуков, использованных в качестве DI.)

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

Использовать инструменты слепо — опасно. Нагородить слои абстракции и индирекности просто, но если это не даёт никаких ощутимых выгод, то они будут лишь вредить, завышая стоимость поддержки и порог входа в проект.

Когда это может быть полезно

Если посмотреть на представленные идеи несколько более абстрактно, то мне представляется несколько ситуаций, когда описанный подход к разработке мог бы быть полезен.

Эксперименты с кодом

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

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

Исследование легаси

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

Необязательно даже коммитить результаты исследований в репозиторий проекта, достаточно сделать это локально, понять структуру взаимодействия модулей и откатить результат. Настоящие изменения в коде после этого могут сильно отличаться в «строгости» реализации и быть намного проще.

Долгоживущие сайд-проекты

Под долгоживущими проектами я имею в виду те, что живут дольше 5–7 лет. Под сайд-проектами я имею в виду те, у которых нет больших ресурсов на ежедневную поддержку и к которым разработчики возвращаются стихийно или систематически, но редко.

Для таких проектов явный дизайн может помочь с двумя вещами:

  • Задокументировать больше знаний о проекте и описать их в коде или наиболее близко к нему.
  • Сделать проект более устойчивым к внешним изменениям вроде хаотичного обновления библиотек или инструментов.

Повсеместный язык и явное описание процессов в коде помогает быстрее входить в контекст задачи после долгого перерыва. Явная структура, направление зависимостей и буферные зоны могут помочь ограничить распространение изменений в коде при обновлении инструментов или инфраструктуры.

Опять же паттерны использования конкретного инструмента будут сильно зависеть от баланса выгод и издержек, но второ- и третьестепенные инструменты можно изолировать так, чтобы их поддержка или замена не отнимали много ресурсов.

Фреймворк поддерживает идею

Если проект построен на фреймворке, который поддерживает описанные принципы, облегчает их реализацию и снижает трение при следовании им, баланс выгод и издержек может измениться. При наличии «направляющих» внутри фреймворка следовать правилам проще, что снижает издержки.

Когда это навредит

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

Проекту это не нужно

Под «не нужно» можно подразумевать разные вещи:

  • Приложение маленькое или не содержит сложной логики;
  • Команда не видит пользы в этих правилах, и они замедлят её работу;
  • Соотношение выгод и издержек выглядит не в пользу подхода;
  • Уже принятые правила написания кода частично или полностью противоречат подходу.

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

Любой подход, методология, идея — это инструмент для достижения какой-либо цели. Если инструмент не помогает её достигнуть, его не нужно использовать.

Прототипы и несложные приложения

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

Ресурсы проекта уже распределены

Одна из описанных идей — изоляция инструментов и фич между собой. Если у проекта достаточно ресурсов на ритмичное обновление зависимостей, инструментов и инфраструктуры, изоляция инструментов может быть не нужна.

Прямая привязка к инструменту иногда может сэкономить больше времени на ежедневном использовании, чем его понадобится для переписывания кода при смене инструмента или обновлении его версии. (Которые к тому же не обязательно случатся.)

Экономить на спичках чаще всего не нужно

Подход противоречит принятым правилам

Если в проекте используется фреймворк или набор правил, который не поддерживает новые идеи, может быть полезнее от них отказаться.

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

Неудачная попытка потратит ресурсы впустую, вгонит разработку во фрустрацию и плохо отразится на состоянии проекта в целом.

Преимущества меньше затрат

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

Если инструмент не приносит пользы, не стоит использовать его

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

Примеры из жизни

На вопрос «Использовал ли я что-то похожее в реальных проектах?» я обычно привожу в пример Тяжеловато. Это приложение для учёта финансов, которое мы сделали в далёком 2015 году. Оно пережило несколько переписываний и в целом было главным полигоном для обкатки новых идей.

Это приложение — тот самый случай, когда проект живёт долго, а ресурсов на работу с ним или поддержку нет.

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

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

Это не рекомендация, как писать код

…А скорее шведский стол идей и мои впечатления от их использования. Не верьте мне на слово, пробуйте всё в песочнице, составляйте своё мнение 🙃

Планы на продолжение

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

  • Применимость подхода с фреймворками типа Next или SvelteKit.
  • Более детальное погружение в функциональное DDD.
  • Типобезопаность и брендирование типов.
  • Код-сплиттинг, маршрутизация и перформанс с React 18.
  • Обработка ошибок в функциональном стиле.
  • Использование с другими JS библиотеками.

Если у вас есть идеи, что могло бы дополнить этот список, пишите на почту или в ишью репозитория проекта. Буду рад обсудить! 👋

Ссылки

Все ссылки на книги, статьи и другие материалы, упомянутые в статье.

Из моих работ

Остальное

Другие части серии