Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В 2020 году, в конце марта, меня пригласили писать бэк на Node.JS для сервиса видеоконференций. Тогда, во времена начала очередного витка мирового спектакля, резко возрос спрос на инструменты, позволяющие вести работу дистанционно. На прототип сервиса, до того простоявший несколько лет практически без дела, из ниоткуда свалился ежедневный трафик в 2000 человек, что породило необходимость начинать в ускоренном темпе развивать продукт и делать деньги.
Спойлер: миллионерами мы так и не стали.
По мере роста объема и связности кода, становилось все труднее держать логику целиком в голове и, соответственно, гарантировать, что после очередных внесенных изменений ни один из вариантов использования не отвалится. И вот, в один прекрасный момент, было решено начать покрывать код тестами. Так началась история поиска идеальной архитектуры.
Спойлер: тестами код мы тоже так и не покрыли.
Давид Хейнемейер Ханссон, создатель фреймворка Ruby on Rails, в своей статье Test-induced design damage утверждает, что те архитектурные изменения, которые необходимо внести в проект, чтобы сделать возможным написание unit тестов для контроллеров, настолько сильно бьют по остальным характеристикам кода, что лучше отказаться от этой идеи в пользу интеграционных тестов.
Реально ли придумать такую архитектуру, которая не заставляла бы чем-то жертвовать? Это можно выяснить при помощи научного метода. Сначала необходимо проанализировать имеющиеся зоны боли, а затем попытаться наложить на код такие ограничения и правила, которые бы исправили ситуацию. На каждой последующей итерации ограничения либо добавляются, либо пересматриваются. Сложность состоит в том, что неправильные решения могут стать причиной появления еще большего числа зон боли. В этом болоте можно увязнуть надолго. Лучший способ проверить адекватность любой гипотезы — поставить эксперимент. И на чем же еще стоит экспериментировать, как не на рабочем проекте?
Спойлер: в итоге мы переписали проект 6 раз. Все ради науки.
Глубокая аналитика текущей ситуации
Как известно, все возможные подходы к программированию были придуманы еще в 60е годы разработчиками на LISP. Все новые, по заверениям авторов, разработки, чаще всего, либо заново открывают давно забытое, либо комбинируют уже имеющееся. Время от времени, еще изобретаются надстройки, но они, как показывает практика, не получают особой популярности и долго не живут. Привет аспектно-ориентированному программированию.
Чтобы собрать архитектуру мечты, нужно понять, какими преимуществами и недостатками обладают уже существующие парадигмы программирования, и каких целей они стремятся достичь. Также рассмотрим, каких результатов ожидает бизнес.
Требования бизнеса
Можно ли не заставлять бизнес выбирать два пункта из трех — "Быстро, качественно, недорого"? Чтобы ответить на этот вопрос, нужно разобраться, в чем причины нарушения каждого из пунктов.
Быстро
Зачастую, на этапе разработки начальной версии продукта, фичи выкатываются очень стремительно. Затем, по мере увеличения объема кода, скорость начинает замедляться. Все чаще приходится признавать, что на предыдущих этапах были приняты неверные решения, и что имеющихся возможностей расширения системы, если они вообще закладывались, недостаточно. В такие моменты появляется выбор: воткнуть костыль, или переписать часть системы так, чтобы она гармонично подходила под новые требования? При принятии решения хорошо помогает вопрос: сколько из грядущих изменений будут опираться на этот участок логики? Если навалить поверх костыля еще кода, то все равно этот костыль, в скором времени, придется раскопать и переписать нормально вместе со всей надстройкой. Если, конечно, вы не ставите целью превращение проекта в тотальный хаос. И чем больше будет навалено сверху кода, тем бóльшая когнитивная нагрузка ляжет на разработчика, который займется переписыванием. Это может привести, в лучшем случае, к невнимательности и появлению новых багов, а в худшем — к выгоранию и дальнейшему падению скорости разработки.
В динамически развивающемся продукте не может существовать конечных решений. Раньше, по неопытности, я старался придерживаться принципа открытости-закрытости, создавая абстракции, в рамки которых, как мне казалось, должны уложиться все последующие изменения в системе. Опыт показал, что при первой же необходимости изменить или дополнить имеющееся поведение, эти абстракции приходится выкидывать, полностью или частично, заменяя их новыми. Тогда получается, что максимальной гибкости системы можно достичь только путем тотального отказа от подобного оверинжиниринга. Чрезмерное использование инверсии зависимостей, метаданных, преждевременные попытки сделать решение переиспользуемым. Все это, на самом деле, приносит больше вреда, чем пользы. Конечно, добиться абсолютной гибкости кода так же невозможно, как невозможно в одно мгновение изменить спецификацию, чтобы учесть все новые требования. Зато можно не плодить лишнего, чтобы потом не пришлось тратить время на избавление от всего этого.
Качественно
Качество кода, в большинстве своем, зависит от опыта разработчиков и строгости используемого набора анализаторов. Хороший код должен быть понятным, гибким, безопасным и, по возможности, коротким. Желательно еще, чтобы запускался. Правильная архитектура при помощи правил структурирует мышление, помогая сеньорам поддерживать качество кода на высоком уровне. Джунов и миддлов, время от времени, все равно придется корректировать.
Однако, это все касается внутреннего устройства кодовой базы. Бизнес же интересует, чтобы все работало безошибочно и, по возможности, быстро. Большинство компаний пытаются снизить число ошибок при помощи добавления тестирования. Оно, безусловно, важно, ведь позволяет, при внесении в код изменений, быть уверенными, что старая логика не поломается. К сожалению, тестирование не может помочь отыскать ошибочные состояния, в которые, при определенной последовательности вызовов, может попасть программа, и которые не были учтены разработчиками. Найти их можно только при помощи моделирования и автоматизированной проверки полученной модели на непротиворечивость. Сейчас у всех на слуху язык спецификаций TLA+, который нацелен на моделирование параллельных систем. Можно ли как-то адаптировать его для описания распределенных систем на JavaScript, или же придется использовать что-то другое? Вопрос требует изучения.
Недорого
Наши знакомые недавно пожаловались, что двухкратное увеличение штата разработчиков совсем не увеличило скорость разработки их продукта. Секрет кроется в функции роста сложности процессов. Во сколько раз больше людей и, соответственно, денег потребуется, чтобы поддерживать вдвое более сложную систему на плаву? В 2 раза больше? В 4? Или, может, в 20 раз? Все зависит от объема технического долга и архитектуры. Допустим, команда регулярно уделяла внимание качеству кода, и технический долг стремится к нулю. Тогда остается сравнивать лишь архитектурные подходы. Хороший подход сохраняет максимальную простоту и предсказуемость системы, что позволяет дольше удерживать в голове полную картину происходящего и, соответственно, управлять процессами силами меньшего числа разработчиков. А это значит, что пропадает необходимость нанимать кучу макак на поддержку. Все высвободившиеся деньги можно и нужно будет потратить на оплату услуг высококвалифицированных хранителей тайн совершенной архитектуры — нас с вами. Недорого разработать продукт не получится