Часть 1. Проблема: дорогие раскопки, хрупкие скелеты, магическое лечение
Легаси плохой не потому что технологии устарели или написан некомпетентными программистами (хотя, каждому хочется назвать предыдущую команду идиотами и переписать всё заново). Легаси дорогой в эксплуатации, сопровождении и развитии потому что каждое изменение требует раскопок. Простая на вид задача превращается в недели работы, потому что знания о системе не лежат в коде явно, они размазаны между слоями, спрятаны в особых значениях, в порядке вызовов, в том что "всегда так работало" - как вопрос "зачем швабры в подвале?" из КДПВ (картинки для привлечения внимания).
Бизнес хочет предсказуемых, дешевых и быстрых доработок, отсутствия инцидентов. Сколько стоит добавить поле в отчет? Сколько стоит поменять формулу расчета? В хорошо структурированном коде это можно оценить. В легаси честный ответ часто "не знаю, пока не раскопаю", а как узнаю окажется неожиданно дорого. Встречал соотношение стоимости изменений 5:1 в разных проектах.
Реинжиниринг это инвестиция в снижение совокупной стоимости владения (TCO) и предсказуемость затрат. И еще много чего хорошего, об этом ниже.
Но есть вторая цель, не менее важная: сделать код доступным для ревью. Ревью это основной инструмент обеспечения качества при вливании изменений в общую кодовую базу. Если код нельзя прочитать за разумное время и с достаточным уровнем понимания, то ревью превращается в формальность. Простой код ограничен по скорости не разработкой, а проектированием и ревью. Кривой код вообще невозможно нормально проверить: слишком много зависимостей и побочных эффектов. Либо сложность в предметной области, а вообще не в коде (скажем, в криптографии).
Рефакторинг и реинжиниринг: разница в масштабе изменений
Рефакторинг по Фаулеру это изменение структуры без изменения поведения. Выделить функцию, переименовать переменную, разнести ответственность. Мелкие шаги, каждый проверяется тестами либо корректность гарантируется IDE.
Реинжиниринг это изменение внутренних контрактов и модели. Ввести типы вместо строк, сделать неявные правила явными, перенести границу между модулями, изменить алгоритм. Внешнее поведение сохраняется, но внутри меняются не только инструкции, меняется способ представления / модель знаний о предметной области.
Рефакторинг работает, когда структура в целом правильная, просто код запутан. Реинжиниринг нужен, когда сама структура порождает сложность. Даже по примерам из книги Фаулера часто видно, что рефакторинга недостаточно.
Откуда берется сложность
Часто код пишут как исследование. Начали писать одну часть, обнаружили локальную логику, зафиксировали здесь. Писали другую часть, обнаружили другое правило, зафиксировали там. Как-то связали между собой и в прод. Исследование не доведено до конечной формы, правила остались локальными, разбросанными по слоям. Такой исследовательский черновик часто застывает в коде и уходит на прод.
В динамически типизированных и даже просто невыразительных языках (Python , PHP и даже Go) код легко писать как исследование или поток сознания. Просто добавляете новый ключ в ассоциативный массив $data['is_special'] = 1 прямо в середине бизнес-логики, потому что прямо сейчас это решает именно эту проблему. Костыль за кос��ылём. Читаем непонятно откуда, обработаем непонятно как, запишем непонятно куда.
Следовательно, доменных правил нет, SRP и изоляции слоёв нет.
Контрактов между блоками в сущности тоже нет. Блоки кода тогда определяют лишь абзацы, а не детали единого механизма. Код может модифицировать данные, созданные 2000 шагов назад в другом файле, в другом модуле. То есть такие языки поощряют отсутствие модульности: границы есть только визуально (функции, файлы), но изоляции нет.

Мой пример: многомерный отчет. По постановке задачи это простая агрегация. Но старый код был неожиданно сложный. Основная проблема оказалась в маппинге понятий из разных предметных областей: имя поля с фронта могло означать иной источник данных, а могло означать подстроку из того же поля таблицы. Один и тот же модификатор (якобы, уточнение параметра) в одном месте менял SQL-запрос, в другом менял форматирование результата. ETL был размазан по всему коду: и в построении выборки, и в презентации.
Поиск этих неявных правил и соглашений занял больше времени, чем реализация. Задача была простой, сложность была в том, что знания о формате данных нигде не были оформлены явно и были неочевидными, спрятанными как в стеганографии.
Это отличается от алгоритмической сложности. Криптография или оптимизация сложны по существу, нужны специфические знания и квалификация. Легаси-сложность другая: задача простая, но код сложный, все слои пронизаны договоренностями, которые берутся на стыке 3-4 предметных областей и потому скрыты от глаза.
Пример неявных договоренностей
разные степени детализации в справочнике ОКЗ (классификаторе занятий)
okz-1 = укрупненные группы (руководители, специалисты высш уровня, ...)
okz-2, 3, 4 = более детальные группы
okz-5 вообще указание на таблицу профессий (ну, так заказчик захотел)
Не сказал бы, что это сложно раскопать в коде, но такая логика проникает во все слои (SQL, пивот, презентационный слой) и создает очень много шума не соответствующего предметной области.
Разница важна для бизнеса. Алгоритмическую сложность можно только изолировать и поручить специалисту. Легаси-сложность можно устранить, и тогда код становится дешевым в изменении и доступным для ревью.
Что делать: выносить неявное в явное
Практически это выглядит как цепочка преобразований на входе. Сырые данные проходят несколько этапов: парсинг, нормализация, валидация, маппинг в доменные структуры. Это ETL в миниатюре.
Anti-Corruption Layer это часть этой цепочки, но на определенном уровне. ACL отвечает за то, чтобы не пропустить внутрь неявные соглашения внешнего мира: что значит пустое значение, какие варианты написания эквивалентны, какой модификатор что означает. После ACL данные уже нормализованы, но еще не обязательно в доменных терминах.
Дальше может быть отдельный слой маппинга в домен. И только потом сам домен, который работает с чистыми типами и не знает про особенности внешнего мира.
Результат: доменная логика становится тривиальной. Ее можно читать, ревьюить, менять. Вся сложность сконцентрирована в цепочке преобразований на границе, где ее можно контролировать и тестировать отдельно.
Не бойтесь гексагональной архитектуры, для изоляции от легаси треша она отлично работает. Причём, адаптером может служить не только код но и преобразования в БД: view, CTE, ...
Второй мой пример: в одном модуле HTML, CSS, JS и SQL были в одном файле. Пока это было смешано вместе, границы обсуждать было бессмысленно. Я разделил вручную: UI отдельно, логика отдельно. После этого стало возможно думать о контрактах между частями, чистом домене и адаптерах, не пропускающих внутрь легаси мусор.
Цепочка преобразований на практике
Парсинг: сырые данные становятся структурой (JSON => объект)
Нормализация: приведение к единому формату (пробелы, регистр, кодировки)
Валидация: проверка, что данные в допустимых пределах
Маппинг: перевод из внешних терминов во внутренние
Домен: чистая бизнес-логика, не знающая про внешний мир
Каждый слой можно тестировать, читать и ревьюить отдельно - важен только его контракт с соседями. На практике 5 отдельных преобразований никто не пишет, объединяют. Но в проектировании нужно понимать, что и зачем.
Где проводить границу
Вопрос не абстрактный, а экономический. Граница правильная, если преобразования на входе простые: работа с форматом, без бизнес-логики. Граница неправильная, если слой преобразований разрастается, в нем появляются условия и исключения, он начинает знать про смысл данных.
Признаки, что граница протекает: в домене появляются поля "для совместимости", изменения домена требуют изменений входного слоя не из-за формата, а из-за смысла, тесты домена тянут внешние структуры данных.
Когда граница протекает, либо расширяешь ее и переделываешь внутренний контракт, либо принимаешь, что изоляция неполная. Второе иногда дешевле, особенно если плохой контракт расползся слишком широко по системе.
Я ожидал, что основной риск на границе фронт-бэк, как и положено "гексагоналке", а на практике большая часть неявных соглашений сидела в схеме и запросах. Когда добавил правильный адаптер, домен перестал "патчить" общие правила.
Экономика
Стоимость реинжиниринга часто недооценивают. Она ближе к сумме стоимости первой версии и стоимости всех доработок за все годы. Доработки это не просто строки кода, это найденные пограничные случаи, это столкновения с реальностью.
Если эти знания упакованы в тесты и типы, реинжиниринг дешевле. Если размазаны по коду и экспертов уже нет, то приходится платить археологией.
Есть еще одна статья, которую редко считают: затраты бизнеса на адаптацию при внедрении. Если новая система ведет себя иначе, чем старая, то пользователи тратят время на переобучение, возникают ошибки, простои. Поэтому внешний контракт я старался сохранять, а внутренние менять так чтобы поведение оставалось прежним.
Почему не переписать с нуля? Джоэл Спольски в классической статье "Things You Should Never Do" разбирает, как Netscape убил себя полным переписыванием. Его аргумент: старый код содержит годы найденных багфиксов и пограничных случаев, которые выглядят как мусор, но на самом деле это знания. Реинжиниринг это попытка сохранить эти знания, но перенести их в явную форму. Не выбросить старый код, а извлечь из него правила и переупаковать. Разница принципиальная: при переписывании вы теряете неявное знание, при реинжиниринге делаете его явным. Практически при внедрении для снижения рисков можно использовать Strangler Fig Pattern: новый код растёт рядом со старым, постепенно забирая функциональность, пока старый не отомрёт сам. Это дороже, чем переписать за выходные, но менее рискованно чем "прыжок веры".
Промежуточный итог
Цель реинжиниринга не чистый код сам по себе. Цель - перевести знания из неявной формы в явную. После этого изменения становятся предсказуемыми: видно, что менять, а что может сломаться, можно оценить стоимость. И код становится доступным для нормального ревью, что напрямую влияет на качество.
Рефакторинг работает без изменений существующей структуры. Реинжиниринг меняет структуру. Это разные инструменты с разной ценой и разным результатом. Путать их при оценке сроков опасно.
Фезерс (Working Effectively with Legacy Code) писал - легаси это код без тестов - писал в 2004, тогда тесты были главным инструментом. Сейчас есть типы, контракты, статический анализ — способов дать гарантии больше. Я бы сказал так
Легаси это код, не привязанный к явным требованиям. Нет способа проверить, что он делает то, что нужно. Потому что неизвестно а что именно нужно. Тесты - один инструмент проверки, который надо создать. Типы - другой. Контракты - третий.
Часть 2. Исследование, выбор и реализация контрактов
Классическая проблема легаси — код писали как исследование. И код как исследование, и контракты как бог на душу положит, если они вообще есть. Черновик застывал и уходил в прод далёкий от законченного качественного решения. Знания оставались неявными.
LLM мозги, освобождённые от утомительной работы кочегаром, переворачивают этот процесс. Теперь есть возможность спроектировать и оценить больше вариантов контрактов, а их реализация вообще дёшевая. Top-down на стероидах.
Как это выглядит на практике:
1. Формулируете гипотезу о границе модуля
2. Описываете контракт: входные данные, выходные данные, инварианты. На более низком уровне можно рассуждать не о контрактах, а о типах и гарантиях которые они описывают
3. LLM генерирует реализацию по контракту (черновик, но с хорошим контрактом ручных доработок почти не потребуется)
4. Смотрите на результат, оцениваете качество контракта и реализации, стоимость оставшейся доработки. Нравится / не нравится.
5. Не нравится — переформулировали контракт, сгенерировали реализацию заново
6. После нескольких итераций контракт стабилизируется. Ограничения: не надо менять чужой домен.
Код больше не исследование предметной области. Все знания из исследования должны быть закреплены в контракте, код только реализация.
Два типа требований к контрактам:
Некоторые требования известны и формализованы: сигнатуры, типы, валидация, обработка ошибок. Их можно описать в промпте, LLM справится.
Другие живут только в голове архитектора. Почему эта граница лучше той? Почему этот контракт будет дешевле в поддержке через год? Это опыт, интуиция, знание истории проекта — трудно формулируемое. LLM не имеет к этому доступа.
Процесс — не «опиши и получи», а исследование, оценка и выбор. LLM расширяет пространство вариантов, человек фильтрует по критериям, которые не полностью вербализуемы.
Что это меняет:
Программист становится архитектором, даже на небольших задачах. Основная работа — формулировать контракты, оценивать варианты, выбирать. Навык «объяснить что нужно» становится важнее навыка «написать как нужно».
---
P.S.
Классический легаси хотя бы честен: видно, что код плохой, но там хоть следы исследований и раздумий, зачатки обоснования. LLM-код — другая проблема. Он выглядит чистым, проходит линтеры, но знания /корректного соответствия требованиям в нём нет. Это не исследование, застывшее в коде, это галлюцинация, которая случайно работает или иногда не работает, кто знает. Отлавливать, изолировать и чинить такое сложнее: нет следов решений, нет истории багфиксов, нет эксперта, который помнит "почему так". Только код, который уверенно врёт.
Я пока не знаю, как развить эту тему. Может быть, это тема для
отдельной статьи
Ловушка LLM-кода
Проблема: выглядит чистым, проходит линтеры, но знания нет
Почему это опаснее классического легаси: нет следов решений, нет «почему так», нет эксперта
Признаки: код уверенно врёт, тесты зелёные но на граничных случаях ломается, копипаста паттернов без понимания контекста
Как ловить: ревью с фокусом на «откуда это знание», тесты на граничные случаи, требовать обоснование в промпте. Но это уже шиза...
Как не попадать: фиксировать и документировать контракт до генерации, человек отвечает за архитектуру
Ссылки
Martin Fowler, Refactoring
Working Effectively with Legacy Code Michael C. Feathers
Joel Spolsky, Things You Should Never Do (про Netscape и цену переписывания):
