Это продолжение статьи Рефакторинг и реинжиниринг легаси

Преамбула

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

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

Содержание статьи

Переписывание легаси — это не одна работа, а четыре.

В этой части статьи я расскажу про первые две.

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

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

Вторая — обеспечение соответствия старому поведению. Пока новая реализация не доказала, что ведёт себя так же, любое улучшение подозрительно. Поэтому рядом с археологией почти сразу появляются characterization tests, golden master, сравнение старого и нового пути, фиксация контрактов. Это не «тестирование в конце», а отдельный поток работы.

Третья — проектирование и проведение изменения. Нужно выбрать границу замены, придумать новую структуру, провести миграцию, встроить новый код в старые контракты. Но эта работа не может идти отдельно от первых двух: без археологии легко потерять смысл, а без проверки на соответствие — незаметно сломать поведение.

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

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


1. Археология

Здесь полезно сразу обговорить два уровня спецификаций.

Нижний уровень: технические спецификации

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

На этом уровне можно довольно много: снять текущее поведение, зафиксировать контракты, построить characterization tests, golden master, сделать адаптеры, перенести модуль на другой стек, перепаковать старую логику в более чистую структуру. Но свобода здесь ограничена: мы видим реализацию, но не знаем, где в ней случайность, а где важное правило.

Именно поэтому максимум, который можно делать уверенно, имея только нижний уровень, — это lift and shift. Не обязательно буквально «тот же код на другом языке», но по сути именно технический перенос с сохранением смысла, который уже есть в системе.

Верхний уровень: бизнес‑спецификации

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

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

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

Код может содержать следы этих решений, но не объясняет их смысл.

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

Почему это важно

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

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

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

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

Архитектурные свойства — часть верхнего уровня

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

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

Поэтому архитектурные свойства нельзя угадывать по исходникам. Их приходится восстанавливать отдельно: из разговоров с людьми, инцидентов, требований эксплуатации, старых решений, ADR, quality attribute scenarios и вообще любых источников, которые объясняют не «как написано», а «что обязано сохраняться».

Где здесь место LLM

LLM отлично работает на нижнем уровне. Он может помочь разобрать код, инвентаризировать правила, найти дублирование, предложить адаптеры, подготовить characterization tests и сделать аккуратный lift and shift.

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

Вывод по п‑ту 1

Переписывание легаси — это работа не с одним слоем, а с двумя.

Нижний уровень отвечает на вопрос, что система делает сейчас. Верхний — зачем она устроена именно так и что в ней нельзя потерять.

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

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


2. Обеспечение соответствия старому поведению

Что должна делать система

  • Обрабатывать POST‑массивы с индексами для каждой строки коллекции.

  • Генерировать DOM‑идентификаторы и имена полей в формате, ожидаемом клиентскими скриптами.

  • Поддерживать скрытые служебные поля (активный таб, версия классификатора, признак редактирования).

  • Передавать в read model (печать, Excel) данные в историческом формате: денормализованные скаляры, склеенные через разделитель коды, булевы как «t»/«f», даты в определённой локали.

  • Сохранять черновики в формате, совместимом с текущим autosave.

Что НЕ должна делать система

  • Не менять порядок А.

  • Не менять семантику Б.

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

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

Методы обеспечения соответствия

Golden master тесты: фиксация поведения до первого изменения: POST на каждый шаг => сохраняем HTML, redirect, ошибки, состояние БД. После рефакторинга сравниваем.

Контрактные тесты на репозиторий: старая и новая реализация пишут в БД; сравниваем SQL‑лог или результат SELECT.

Проверка read model: печать и Excel генерируются из новых данных; сравниваем с эталонными файлами.

Сложности проверки соответствия

Легко проверяемые: сигнатуры методов, типы DTO, схема таблиц, составные ключи, явные API‑контракты.

Трудно проверяемые (и именно они определяют выбор scope):

  • Фронтенд ожидает не просто JSON, а конкретную семантику: порядок вызовов, структуру DOM, CSS‑классы, моменты появления элементов, тайминги.

  • Read model зависит от порядка, формата, округления, локали, пустых строк. Печать и Excel часто держатся на неявных соглашениях.

  • Сессия как источник состояния: другие модули могут неявно полагаться на наличие определённых ключей в сессии.

Scope изменений

Здесь возникает интересный вопрос: на каком объеме изменений стоит остановиться.

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

В этом вопросе мне помогали 2 приема

  • spike, когда я делаю пример реализации (обычно, с помощью LLM) чтобы оценить, насколько сложно и рискованно двигаться в этом направлении, качество кода, размер и хрупкость адаптеров

  • оценки сложности проверки контрактов in‑out и out‑in

Проверить контракт просто, если:

  • код статически проверяемый,

  • компилятор дает гарантии,

  • правила выражены в типах

Пложно, если:

  • это раздел на 2 системы (фронт‑бэк, сетевые внутренние или внешние контракты)

  • логика непонятна, не формализована или ее сложно формализовать (А после Б, …)

Если адаптеры безобразны, а расширение scope не несет больших рисков, то обычно это оправдано. Главное, чтобы вам хватило ресурсов и не поджимали сроки.


Терминология:

  1. Spike — короткое исследование для снижения неопределённости, а не для поставки фичи. https://agilealliance.org/glossary/spike

  2. Seam — место, где можно изменить поведение системы без тотального переписывания (термин из книги Michael Feathers, Working Effectively with Legacy Code).

  3. Characterization Tests — тесты, фиксирующие текущее поведение легаси до изменений Michael Feathers, Working Effectively with Legacy Code.

  4. Golden Master — подход, при котором результат старой системы сохраняется как эталон и сравнивается с новой реализацией.

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


  1. vsinyavsky
    03.06.2026 17:42

    Разделение на 4 активности - то, чего жанру обычно не хватает: все застревают на археологии, а "обеспечение соответствия" и "проектирование изменения" сливают в один туман. Спасибо, что растащили.

    Тезис "глубина переписывания зависит от уровня знания, а не от смелости команды" - это ровно мой опыт. Тащил миграцию edtech-платформы с Rails на .NET, 10 месяцев. Где у нас был доступ к верхнему уровню - к людям, которые помнили почему фича сделана именно так - получался настоящий реинжиниринг. Где доступа не было - честный lift-and-shift через адаптер, который стыдно показывать, но он работал и не врал.

    И мой главный косяк ровно в вашу тему: за все 10 месяцев мы провели одну Event Storming-сессию. Одну. То есть систематически недоинвестировали в тот самый верхний уровень, а потом гадали по коду - путь в никуда, как вы и пишете. Хуже того: команда искренне считала, что замысел уже восстановлен, хотя восстановлена была от силы половина. Вот эта переоценка собственного знания - по-моему, главная ловушка, и метрики у неё нет.

    Как вы для себя ловите момент "знаю достаточно, чтобы лезть глубже lift-and-shift"?

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


    1. Dhwtj Автор
      03.06.2026 17:42

      Спасибо за проявленный интерес

      реальные кейсы

      Это тяжело: кусочек тут, акцент там... Статья начнёт распадаться.

      Ну в двух словах расскажу.

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

      Но 4 года назад программе повезло наконец то встретиться с сильной командой из 2 человек)

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

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

      Сложные случаи:

      Первая попытка сконцентрировать разбросанную логику в домене, валидировать с экспертами короткий и понятный код домена и тем самым обеспечить надёжность - провалилась. Слишком широкий замах и незнакомый стек (включая фронт, который слишком сильно зависел от структур данных). Смешал несколько технологий и запутался в mutable shared state. Переоценил возможности LLM.

      Пришлось признать поражение согласиться с руководством, милостиво выбросившим байку что всё отдадут другой компании и вкладываться не нужно. Это было моральное спасение. Я свернул scope до минимального, добавил нужные валидаторы и это жило ещё полгода в виде transaction script.

      Потом стало больше времени (а руководство могло решить или перекинуть в другой проект или вообще "оптимизировать") и набравшись сил я всё-таки спроектировал DDD с элементами гексагональной архитектуры. Объём релиза 10.000 строк, мелкими кусками не получалось. Из-за рисков релиз готовился и проверялся полгода.

      Новые грабли:

      Адаптеры кривые, мусор остался в домене. Нашёл красивое нестандартное решение - адаптер в БД, через view.

      Потом столкнулся с каскадными проблемами легаси, когда одно кривое решение было причиной другого, третьего... Post кидал данные с формы данные в нелепом виде, уродливый адаптер защищал домен, но хрупко... При попытке изменить структуру внезапно отвалился богом забытый JS, неявно подписанный на неё и на HTML теги. Сделал частично. Но понравилась идея, что agile spike помогает провести быструю разведку боем и посмотреть не стоит ли пробить ещё один уровень каскадно-кривых легаси контрактов и привести систему в порядок, убрав хрупкость адаптеров ценой увеличения объёма изменений.

      Потом был интересный кейс, когда пробовал LLM агентов. Да, нашли все зависимости кода, предложили решение, но архитектурно хрупкое: кривая структура была в центре зависимостей. Когда хотел её исправить шла стрельба дробью. Потом ещё патч. В результате n² изменений. То есть антихрупкость, low coupling high cohesion - ну никак не дело LLM. Это дело архитектора определить часто меняющиеся участки и сгруппировать их в домен.

      И грабли с тестированием command части CQRS - надо же смотреть изменения в БД. И в старой системе тоже, а там никаких слоев, тесты вставить некуда, только трудоемкие ручные. Сделал свой proxy + loger get/post + dbdiff. Хотя не сильно помогло, но отловил пару ошибок.

      Ну вот, основные интересные кейсы, которые встретил. Если кратко.

      Доменную экспертизу вырастили сами. Теперь мы знаем процессы лучше чем кто-то другой. Доменных экспертов тупо нет кроме нас.

      знаю достаточно, чтобы лезть глубже lift-and-shift"

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


      1. vsinyavsky
        03.06.2026 17:42

        Спасибо, что развернули так подробно, это наверное тянет на отдельную статью ))

        И ценно, что вы не спрятали провал первой попытки: почти все рассказывают про легаси как "пришёл и навёл порядок", а в реальности всегда что-то типа этого... слишком широкий замах, отступление до transaction script, и только со второго-третьего захода DDD. У меня на похожем проекте миграции было так же: первый замах оказался шире, чем стек и моё знание домена позволяли это, пришлось резать скоуп и отступить. Адаптер через view в БД - интересно, приём украду )

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

        И раз вы сами вывели на то, что LLM провоцирует переоценку знания, накину еще с другой стороны...

        Доменных экспертов тупо нет кроме нас.

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


        1. Dhwtj Автор
          03.06.2026 17:42

          это знание лежит где-то кроме двух ваших голов?

          нет, заказчик осознанно не хочет брать на себя это
          да, если команда сменится, то все знание уйдет с командой (документация и код мало чем помогут)
          для долгоживущих систем нужны долгоживущие core команды

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

          ну да. ADR и прочее я пишу для будущего себя, но не надеюсь что кто-то еще поймет (я вообще мало знаю специалистов по DDD)

          но на уровне кода стало значительно понятней всем - вот сегодня мой баг поправил другой разработчик в легкую

          правка идёт дробью на n² мест.

          правкИ. n правок на n мест
          ну или n*k - просто показал масштаб

          это наверное тянет на отдельную статью

          Я люблю читать про чужой опыт (случаи, боль, шрамы), но про свой писать почему-то не люблю.

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

          Адаптер через view в БД - интересно, приём украду )

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

          последнее время много логики уходит в PL/SQL потому что так удобнее тестировать на реальных данных - вызываешь в dbeaver и смотришь глазами


    1. Dhwtj Автор
      03.06.2026 17:42

      нашли баг посреди миграции

      Нет. Но откат релиза невозможен, какие-то кривые настройки Git и Jenkins. Было стрёмно.

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

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

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

      переоценка собственного знания - по-моему, главная ловушка, и метрики у неё нет

      Зато LLM очень провоцирует такую переоценку и уверенность что всё уже понятно. Такая вот психологическая ловушка


  1. Dhwtj Автор
    03.06.2026 17:42

    Я писал короткие тире (-) и обычные кавычки (").

    Это у нас теперь Хабр сам делает статью подхожей на нейрогенерацию?