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

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

Лирическое вступление

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

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

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

Способность изменяться

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

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

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

Не загадывай наперед

Старая поговорка гласит: "Хочешь рассмешить бога - расскажи ему о своих планах". В разработке ПО она подходит идеально. Самая опасная ошибка в архитектуре - пытаться детально спроектировать будущее, которое туманно и неизвестно. Мы все были там: сидишь на старте проекта, пытаешься учесть все возможные функции, расширения, нагрузки. Думаешь: а вдруг через год понадобится поддержка нескольких валют - надо сразу заложить? Или решаешь: пишем на вырост, заложим гибкий плагинный механизм - мало ли что.

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

Принцип YAGNI (You Aren't Gonna Need It - «тебе это не понадобится») в полной мере проявляется именно в гибкости продукта. Если вас триггернуло упоминание гороскопов, то вы точно должны понять: мы не можем предсказать будущее. Самые мудрые разработчики (даже те, кто чихает после того, как скажут что-то правдивое) не способны точно сказать, куда повернёт бизнес и как система будет выглядеть через пару лет. Поэтому нет смысла загадывать наперёд и усложнять дизайн под гипотетические требования. Лучше сделать минимальное работающее решение сегодня и улучшать его по мере поступления новых требований.

Это не призыв бросаться в код бездумно - думать о будущем полезно. Но есть грань между гибкостью и overengineering. Не нужно делать универсальный швейцарский нож сразу. Нужно делать так, чтобы потом было легко переделать. Суть не в простоте кода, а в том, каких усилий будет стоить переделка и какие риски она повлечёт. Код не должен мешать будущим изменениям, а должен им способствовать. Особенно это касается тестов. Забегая вперёд, скажу: именно тестам я отдаю ключевую роль - об этом дальше (ладно, поймали, это я могу предсказать).

Не загадывай наперед даже ты

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

Преждевременная универсальность

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

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

Золотое правило здесь простое: обобщать имеет смысл только когда что-то повторилось три раза. Проще говоря:

  • Один раз - вообще не повод для абстракции.

  • Два раза - слабый сигнал: возможно, назревает третий.

  • Три раза - всё, пора выделять общий механизм.

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

Смирись: всё сразу не поймёшь

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

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

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

Итерации важнее революций

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

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

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

Прививка фич: grafting

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

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

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

API не высечен в камне

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

Особенно это верно для внутренних API между сервисами. Ну не бывает так, чтобы за пять лет ни один эндпоинт не поменял сигнатуру. Даже если сам контракт стабильный, появляются новые версии, старые где-то отходят, или добавляются параметры. Зрелый продукт живёт в режиме постоянного перестроения, и API - часть этой жизни.

Как с этим жить? Во-первых, версионирование. Смиритесь, что рано или поздно придётся пилить v2, v3... Лучше заранее заложить возможность нескольких версий API, чтобы не ломать клиентов каждый раз. Во-вторых, политика устаревания: разрабатывайте стратегию, как вы будете выводить старую функциональность. Пусть даже через "Deprecated" и год поддержки перед выпиливанием. Это лучше, чем пытаться навечно поддерживать первый вариант интерфейса, боясь гнева пользователей. Пользователи, кстати, тоже привыкают: лучше дать им чёткий план "старый API умрёт через N времени, переходите на новый", чем молчком менять поведение, а потом стыдливо избегать этой темы.

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

Тесты - страховка и свобода

Не устану повторять: тесты - лучшие друзья гибкого кода. Когда у вас сотня модулей и сервисов, легко случайно нарушить чей-то контракт или взаимосвязь. Тесты тоже надо поддерживать, и плохие тесты могут мешать развитию не меньше, чем плохой код. Но без хоть какого-то набора тестов жить гораздо страшнее. Каждый рефакторинг превращается в рулетку: "прокатит/не прокатит". А страх менять код - главный убийца гибкости. Мой личный выбор, проверенный годами на разного рода сервисах, это Testing Trophy. Это подход к покрытию кода тестами, при котором акцент делается на интеграционные тесты с фокусом на наблюдаемое поведение. А если тесты отвязаны от реализации, то саму реализацию можно менять хоть каждый день и это будет безопасно.

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

One pile

Про "дать коду отлежаться" это я не просто так. Есть одна интересная практика - One pile, описанная в книге Кента Бека "Tidy First?". Суть в том, что понимать код сложнее, чем писать. Если код разбит на слишком много мелких частей, полезно сначала объединить его в одно целое, чтобы увидеть общую структуру, а уже потом разносить по методам и модулям. Особенно это полезно в ранней стадии, когда задача и её границы ещё туманны.

Заключение

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

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


  1. ALexKud
    12.08.2025 05:47

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


    1. Avvero Автор
      12.08.2025 05:47

      Хотелось бы почитать про ваш опыт с dsl поподробнее. Интересно что за продукт и как он развивался со временем.


    1. ruomserg
      12.08.2025 05:47

      Вот прямо обратный опыт имею, сказал бы я... Всякий раз когда кто-то изобретает специфичный язык конфигурации или расширений - это каждый раз кончается тьюринг-полным языком программирования (с условиями, циклами, и т.д.) - но без нормальной инструментальной поддержки базового языка (ни тебе отладчика, ни тебе профайлера). Плюс порог для входа - нельзя просто нанять программиста на Java/C#/C++ - надо научить его синтаксису и идиомам нового встроенного языка. Потому что каждый из этих новоизобретенных языков уникален, и опыт работы с ним есть у пяти человек во всем мире...


      1. vadimr
        12.08.2025 05:47

        Так необязательно писать интерпретатор нового языка. Можно использовать базовый язык, макрорасширяемый в DSL.


        1. ruomserg
          12.08.2025 05:47

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


          1. vadimr
            12.08.2025 05:47

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


  1. nv13
    12.08.2025 05:47

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

    Yagni вроде не про дизайн, а про функциональность? Не надо лишних функций не тождественно давайте будем считать, что сеть не падает, например.

    Что не устраевает лично меня во всех этих драях и кисах, так это то, что рядом с ними всегда политиканство и беда) К примеру mvp - ок, но не тащите демо в прод под видом mvp, а если это реально mvp, то вы, большие начальники, не делайте больших глаз узнав, что там половина внутренностей лишь в проекте.

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

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


    1. Avvero Автор
      12.08.2025 05:47

      Yagni вроде не про дизайн, а про функциональность?

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

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

      допустим одну и ту же задачу решают девелоперы с разной экспертизой

      Они могут оба оказаться неправы, независимо от экспертизы.