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

Преамбула

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

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

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

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

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

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

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

Вторая - обеспечение соответствия старому поведению. Пока новая реализация не доказала, что ведёт себя так же, любое улучшение подозрительно. Поэтому рядом с археологией почти сразу появляются 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 не несет больших рисков, то обычно это оправдано. Главное, чтобы вам хватило ресурсов и не поджимали сроки.


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

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

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

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

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

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