Миграция без отдельного квартала на переделку
Переход к новой архитектуре можно вести вместе с текущими задачами: заменять старую реализацию того модуля, который всё равно приходится менять. Это увеличивает объём текущей работы, зато позволяет проверить новую реализацию на нужной возможности. Порядок перехода будет зависеть от того, какие части системы понадобилось менять: до редко меняющегося модуля очередь может так и не дойти, хотя ради него придётся поддерживать старый код.
Такую миграцию стоит оценивать вместе с обычной разработкой. Если в план заложить только время на новую возможность, затраты на перенос останутся неучтёнными. Понадобится время и на подготовку границ между реализациями, и на их совместную работу, и на удаление старого пути.
Прашант Пашам описал переход, при котором новые задачи включали перенос затронутого кода. Время на миграцию выделяли вместе с нужными изменениями. На таком же принципе построим учебный пример с придуманными оценками: возьмём небольшую систему заявок и доведём план до решения о последнем старом модуле.
Что мешает в старом коде
В системе можно создать заявку и назначить её сотруднику. Раз в месяц она готовит отчёт о работе команды. Внутри эти возможности разделены на модули, но все они читают команду и права пользователя из общего контекста веб-запроса. Чтобы вызвать такую логику из фоновой задачи, приходится изображать запрос с нужным пользователем.
Сейчас нужно добавить назначение по навыкам сотрудника. Следом запланировано создание заявок из входящей почты. Оба изменения заставляют снова разбираться, откуда взялась текущая команда и кто имеет право действовать от её имени. Одной новой папкой эту зависимость не убрать.
Цель переделки конкретнее: правила работы с заявкой получают нужные данные явно и могут выполняться без веб-запроса. Обработчик запроса проверяет доступ, собирает данные и вызывает правило. Фоновая задача делает это со своими полномочиями. Сохранение результата остаётся отдельным действием того, кто вызвал правило.
Нам не требуется одновременно переносить данные в другую базу. Существующая таблица заявок остаётся источником данных для обеих реализаций. Это позволяет разбирать замену кода отдельно от переноса хранения. Если бы целью была ещё и замена схемы данных, у перехода появились бы другие промежуточные состояния, и нынешней оценки было бы недостаточно.
Где проходит первая граница
Я бы начал с назначения сотрудника. Это запланированное изменение и достаточно узкий участок: по данным заявки и доступным сотрудникам выбрать подходящего исполнителя. Результат можно проверить до сохранения.
Перед старым модулем появляется одна точка вызова. Снаружи у неё понятный вход: заявка и сведения, необходимые для выбора исполнителя. Внутри пока работает прежний код. Затем за этой же границей появляется новая реализация, которая получает все данные аргументами. Выбор реализации находится здесь же, поэтому вызывающим участкам не приходится повторять условие перехода.
Обработчик запроса
↓
Проверка доступа и подготовка данных
↓
Выбор реализации назначения
↓
Старое правило или новое правило
↓
Сохранение выбранного сотрудника
У схемы есть важная деталь: правило возвращает решение, а запись выполняется один раз после выбора реализации. Если для сравнения запустить оба варианта, оба не должны назначить сотрудника в базе и отправить ему письмо. Иначе эксперимент по проверке результата сам изменит поведение системы.
В учебном случае удаётся отделить вычисление назначения от этих действий. Для уже поддерживаемых случаев обе реализации получают одинаковые исходные данные, а результаты сравниваются без повторной записи. Новое требование о навыках проверяется отдельно: там ожидается осмысленное отличие, и равенство старому результату уже не является критерием правильности.
Если старый модуль невозможно вызвать без сохранения и отправки письма, такую подготовку придётся сначала выполнить. Это часть стоимости перехода. Запускать обе реализации на реальных заявках в надежде потом разобраться с последствиями я бы не стал.
Что именно стало дороже
Пусть оценка добавления навыков в прежний модуль составляет три рабочих дня разработки. Вариант с переносом занимает пять: нужно определить вход правила, отделить запись и проверить обе ветки. Это не пять календарных дней до выпуска, а сравнение объёма работы при одинаковом составе задачи.
Кроме того, два дня уйдут на общую подготовку перехода: возможность выбирать реализацию и видеть, какие вызовы ещё идут старым путём. Эта работа нужна до первого переключения. Она не исчезнет оттого, что мы решили не заводить большой проект миграции.
Команда сводит оценки подготовки и ближайших изменений в одну таблицу:
| Работа | Без переноса, рабочих дней | Вместе с переносом, рабочих дней | На что уходит дополнительное время |
|---|---|---|---|
| Общая подготовка перехода | 0 | 2 | Выбор реализации и наблюдение за вызовами |
| Назначение по навыкам | 3 | 5 | Явные входы правила и отделение записи результата |
| Создание заявки из письма | 2 | 3 | Новый вход в модуль создания без имитации веб-запроса |
| Дополнительное поле в месячном отчёте | 1 | 8 | Разбор зависимостей отчёта и удаление его обращения к общему контексту |
Для назначения и создания заявки получаются восемь дней вместо пяти. Ещё два дня общей подготовки дают десять дней работы. Эти числа нужны в разговоре о плане целиком. Если в плане оставить только время на новые возможности, недостающие пять дней позже будут выглядеть как внезапная медлительность разработчиков.
Мне кажется честнее сказать: «Мы сделаем обе возможности и перенесём эти модули за десять дней разработки. Пять дней из них относятся к переходу. Если сейчас можем выделить только пять, нужно менять состав работы». Дальше уже можно обсуждать, что ждёт и ради чего. Предложение «сначала согласуем короткую оценку, потом аккуратно улучшим код» лишает остальных участников этого выбора.
Дополнительное время нельзя заранее выразить одним процентом для всех задач. На назначение нужны два дополнительных дня, на создание заявки один. Подготовленный вход в новую логику может облегчить следующий перенос, а неожиданная зависимость от общего состояния увеличит его цену. Процент, взятый из чужого кейса, таких различий не объясняет.
Совместимость тоже становится работой
Проверка результата и удаление старого пути входят в эти оценки переноса. Дополнительную работу по поддержке двух реализаций придётся учитывать отдельно: она зависит в том числе от длительности перехода.
Пока обе реализации существуют, исправление ошибки может понадобиться обеим. Допустим, система позволяет назначить заявку сотруднику в отпуске, хотя по правилам он недоступен. Если часть команд ещё использует старый вариант, недостаточно поправить только новый. Иначе пользователи получат разные правила в зависимости от состояния переключателя.
Новые возможности не всегда нужно делать в обеих ветках. Назначение по навыкам можно включать только вместе с новой реализацией и явно учитывать это в доступности функции. Но тогда возвращение команды на старый вариант уберёт эту возможность. Откат должен иметь понятный результат для людей, а не только успешное переключение настройки.
В нашем примере результат нового правила записывается в том же формате, что и результат старого. Поэтому возврат на старое правило не требует преобразовывать уже созданные заявки. Это отдельное условие, которое мы проверяем до переключения. Если в данных появятся значения, которых старый код не понимает, одной настройки для безопасного возврата больше не хватит.
Я бы договорился, кто из разработчиков следит за совместимостью двух путей и проверяет, когда старый можно удалить. Иначе через несколько месяцев новый разработчик увидит два рабочих пути и будет вынужден поддерживать оба. Для него слово «временно» в старом обсуждении ничего не изменит.
В плане я бы отдельно отмечал, какую работу ещё выполняет старый модуль. Например: назначение уже перенесено, однако одна фоновая задача ещё вызывает старую функцию напрямую. Значит, граница обходится и удалять старый код рано. Такой факт полезнее сообщения «модуль почти готов».
Когда перенос слишком дорог для текущей задачи
Вернёмся к месячному отчёту. Добавить поле можно за один день, а перенос вместе с этой правкой оценён в восемь. Причина не в размере поля. Отчёт по пути обращается к старому загрузчику пользователей и использует общий контекст в нескольких вложенных расчётах. За добавлением одного поля обнаружилась переделка нескольких связанных частей.
Здесь я бы остановил автоматическое правило «раз тронули, переносим». Иначе одна небольшая задача получит расходы, которые относятся ко всей оставшейся переделке, а её заказчик будет пытаться отменить миграцию ради своего поля.
Сначала стоит проверить, нельзя ли уменьшить сам участок. Может быть, отчёту нужен только список доступных команд, и достаточно вынести получение этого списка из расчёта. Тогда появится отдельное полезное изменение с меньшей ценой. Но это нужно установить по коду. Нарезать оценку на несколько задач, которые всё равно нельзя выпустить по отдельности, не значит уменьшить перенос.
Предположим, после разбора границу уменьшить не удалось. В учебном плане команда добавляет поле в прежнем модуле и отдельно записывает условие завершения переноса отчёта. Это явное исключение из правила, которое обсудили до реализации. Остальные модули продолжают переходить вместе с запланированными изменениями.
Исключение само по себе не отменяет подход. Гораздо существеннее, во что превращаются следующие задачи. Если почти каждая задача требует большой общей подготовки, включать эту работу в оценку ближайшей функции неудобно. Стоит выделить эту подготовку в самостоятельную работу и оценить снова. Возможно, выбранная граница проходит не там, где система действительно делится.
Последнему модулю может понадобиться отдельное время
Через несколько выпусков создание и назначение работают через новые входы. Старые функции этих модулей удалены. Месячный отчёт исправен и почти не меняется. Новое поле может понадобиться через год, а может не понадобиться вообще.
Если целью было удобнее развивать создание и назначение, команда уже получила этот результат. Можно сознательно оставить отчёт со старым контекстом и считать поддержку этого остатка обычной работой. Тогда в планах больше нет обещания полностью удалить общий контекст. Желание убрать его когда-нибудь не делает оставшийся модуль частью активной миграции.
Но в нашем учебном случае общий контекст нужно удалить из приложения целиком. На нём держится отдельный способ подготовки фоновых задач, и его сохранение заставляет поддерживать старый путь запуска. Значит, отчёт придётся перенести независимо от спроса на новые поля.
После изменения поля команда заново оценивает оставшийся перенос отчёта в семь дней. Это время выделяют в следующем плане и вместе с руководителем решают, какую другую задачу отложить. Постепенная миграция уменьшила последний отдельный участок, но не создала для него бесплатное время.
Можно было с самого начала выделить время на миграцию и точно так же выпускать её по модулям. Тогда очередность не зависела бы от ближайших запросов на новые возможности: к отчёту приступили бы раньше, если он удерживает общий контекст. За это время команда сделала бы меньше новых возможностей. Отдельный бюджет не требует переключать всю систему за один выпуск.
В нашем варианте первые переносы получили время вместе с нужными возможностями. Но ради отчёта всё ещё приходилось поддерживать общий контекст и прежнюю подготовку фоновых задач. Выделив семь дней отдельно, команда перестала ждать нового запроса на изменение отчёта. При сравнении планов стоит учитывать, как долго придётся поддерживать эту оставшуюся часть старого кода, а не только время на написание новых модулей.
Что означает «перенесли»
Новая реализация может по-прежнему читать общий контекст через вспомогательную функцию. А старую функцию может продолжать вызывать фоновая задача, которую не затронул перевод веб-запросов. Одного переключения запросов недостаточно, чтобы удалить прежний код. Поэтому для нашего перехода я бы проверял завершение отдельно для каждого модуля:
| Проверка | Какой незаконченный участок она обнаруживает |
|---|---|
| Все известные входы вызывают новую реализацию | Фоновую задачу или ручной запуск, которые забыли переключить |
| Новое правило получает данные явно | Скрытое чтение прежнего контекста через вспомогательную функцию |
| Результат проверен на характерных случаях | Потерянное условие, которое не встретилось в первых новых заявках |
| Старый путь и его переключатель удалены | Совместимость, которую продолжают оплачивать после перехода |
Для месячного отчёта нужны примеры с данными за полный период. Отсутствие обращений к старому коду в течение обычного дня здесь мало что сообщает. Можно проверить расчёт на сохранённых обезличенных данных в контролируемом запуске и сравнить результат, включая пустой период и записи на границе месяца. Затем убедиться, что плановое задание действительно использует новый вход. Длительность наблюдения выбирается под способ работы модуля, а не общей цифрой для всех миграций.
Удаление старого пути тоже меняет возможности команды: после него прежнего мгновенного переключения уже нет. До этого момента проверяем новый результат и согласуем, как будем исправлять отказ после удаления. Обещание бесконечно хранить старую ветку на всякий случай оставило бы миграцию незаконченной.
В конце нашего примера отчёт получает новый вход, фоновая задача перестаёт изображать веб-запрос, общий контекст больше никому не нужен. Его можно удалить вместе со старой подготовкой запуска. Следующее поле отчёта уже оценивается как обычное изменение одной реализации. Дополнительные дни ушли на то, чтобы эти части можно было менять и запускать без имитации веб-запроса. А старая реализация больше не требует ни исправлений, ни объяснений новому разработчику.