Статьи

Стратегия спринтерской многоножки: как улучшить программное обеспечение, не ломая его

Переиздано с blog.iterate.no .

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

Извлеченные уроки: избегайте лягушачьих прыжков. Вместо этого следуйте стратегии Кент Бека Спринтинг Многоножка — действуйте небольшими, безопасными шагами, которые не нарушают код. Внедряйте его в производство часто, желательно ежедневно, чтобы заставить себя делать действительно небольшие и действительно безопасные изменения. Не изменяйте одновременно несколько несвязанных вещей. Не думайте, что вы знаете, как работает код. Не думайте, что ваше намеченное изменение является простым. Тщательно тестируйте (и не доверяйте своему тестовому комплекту). Пусть компьютер предоставит вам обратную связь и достоверные факты о ваших изменениях — запустив тесты, выполнив код, запустив код в производстве.

Что произошло? У нас есть пакетные задания, свойства конфигурации которых могут быть установлены через (1) аргументы командной строки или (2) специфичные для задания или (3) общие записи в файле. Задания, используемые для доступа к нему через статический вызов Configuration.get("my.property"). Поскольку глобальная автоматически загружаемая конфигурация делает невозможным модульное тестирование заданий с разными конфигурациями, мы хотели заменить синглтон экземплярами конфигурации, которые были переданы.

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

Путь к болотам

Мы попытались произвести это:

Рис.1: Большой неудачный рефакторинг

Мы начали с того, что заменили статические Configurationтри экземпляра класса, каждый из которых имеет только одну ответственность ( SRP)). Затем мы изменили каждую работу, чтобы принять / создать ту, в которой она нуждалась. Мы также переименовали некоторые аргументы и свойства командной строки во что-то более понятное и внесли несколько небольших улучшений. Наконец, мы заменили (неправильное) использование системы конфигурации для хранения информации о том, где задания закончили свою работу в последний раз («закладки»), чем-то лучшим. Как побочный эффект, были также некоторые другие изменения, например, конфигурация по умолчанию больше не загружалась из местоположения на пути к классам, а из относительного пути файловой системы, и аргументы командной строки обрабатывались немного по-другому. Все тесты, кроме одного, прошли, и все выглядело хорошо. Это было более сложно (изменение потребовало других изменений, оригинальный дизайн был слишком упрощенным и т. Д.) и таким образом заняло больше времени, чем ожидалось, но мы наконец справились.

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

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

Лучше?

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

Рис.2: Представьте экземпляр конфигурации

В качестве первого шага (см. Рис. 2), мы могли бы оставить почти все как есть, только ввести временный класс ConfigurationInstance1 с тем же API (чтобы упростить замену), Configurationно не являющийся статичным, и изменить  Configurationдля пересылки всех вызывает свой собственный (singleton) экземпляр 2 of ConfigurationInstance. Небольшое, легкое, безопасное изменение. Затем мы можем изменить наши задания, одну за другой, чтобы использовать  ConfigurationInstanceполученный  Configuration.getInstance()по умолчанию, а также альтернативно принять его экземпляр при создании. (Обратите внимание, что в любое время в течение этого процесса мы могли — и должны — развертывать на производстве.)

Код будет выглядеть так:

class Configuration {
   private static ConfigurationInstance instance = new ConfigurationInstance();
   public static ConfigurationInstance getInstance() { return instance; }
   public static void setTestInstance(ConfigurationInstance config) { instance = config; } // for tests only
   public static String get(String property) { instance.get(property); }
}
class ConfigurationInstance {
   ...
   public String get(String property) { /** some code ... */ }
}
class MyUpdatedJob {
   private ConfigurationInstance config;
   /** The new constructor */
   MyUpdatedJob(ConfigurationInstance config) { this.config = config; }
   /** @deprecated The old constructor kept for now for backwards-compatibility */
   MyUpdatedJob() { this.config = Configuration.getInstance(); }
   doTheJob() { ... config.get("my.property.xy") ... }
}
class OldNotUpdatedJob {
   doTheJob() { ... Configuration.get("my.property.xy") /* not updated yet, not testable */ ... }
}

Как только это будет завершено для всех заданий, мы можем изменить создание экземпляров заданий для передачи в ConfigurationInstance. Далее мы можем удалить все остальные ссылки Configuration, удалить его, и , возможно , переименовать ConfigurationInstanceв Configuration. Мы могли бы регулярно развертываться в нашей тестовой / промежуточной среде и, в конечном итоге, в рабочей среде, чтобы убедиться, что все по-прежнему работает (что для изменений должно быть минимальным).

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

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

1 ) Обратите внимание, что Java не позволяет нам иметь как статический, так и нестатический метод с одним и тем же именем, поэтому нам потребуется либо создать методы экземпляра в Configuration с другими именами, либо, как мы это сделали, создать другой класс. Мы хотим сохранить одинаковые имена, чтобы сделать переход от статической конфигурации к конфигурации экземпляра простым и безопасным поиском и заменой (« Configuration.» на « configuration.» после добавления поля configurationв целевой класс.) «ConfigurationInstance», по общему признанию, является некрасивое имя, но потом его легко и безопасно изменить.

2 ) Вариант рефакторинга «  Представить делегат экземпляра», описанный в оригинальной книге Майкла Фезерса « Эффективная работа с устаревшим кодом»

Принципы безопасной эволюции программного обеспечения

Изменение устаревшего — плохо структурированного, плохо протестированного — кода является рискованным, но необходимым для предотвращения его дальнейшего ухудшения, а также для его улучшения и, следовательно, снижения затрат на его обслуживание. Вполне возможно минимизировать риск — если мы будем осторожны и будем действовать небольшими, безопасными шагами, регулярно проверяя изменения (путем тестирования и развертывания на стадии / производстве).

Что такое небольшое и безопасное изменение? Это зависит. Но хорошее эмпирическое правило может заключаться в том, что это такое изменение, что (1) все остальные могут ежедневно объединяться и что (2) затем может быть развернуто для подготовки (и, например, через день, для производства). Если должна быть возможность объединять его каждый день, то он должен быть относительно небольшим и неразрушающим. Если он должен развертываться ежедневно, он должен быть безопасным и обратно совместимым (или автоматически переносить старые данные). Если ваше изменение больше или рискованнее, оно слишком велико / рискованно.

Стоимость и преимущества безопасности : безопасное изменение программного обеспечения нелегко и «дешево». Это требует от нас серьезного размышления, иногда сделать его более уродливым, тратить ресурсы на поддержание (временной) обратной совместимости. Кажется, гораздо эффективнее изменить код, не прибегая к такой безопасности, — но только до тех пор, пока вы не столкнетесь с неожиданными проблемами и не потратите дни, пытаясь исправить положение, по колено в дерьме (то есть сломанная база кода постоянно меняется вашими товарищами по команде). Он похож на TDD — он медленнее, но окупается благодаря тому, что вы не тратите время на отладку и устранение неполадок, связанных с производством. (Показывать время, которое вы * не * теряете в своей команде или руководстве, к сожалению, сложно.)

Спринтинговая многоножка — серия крошечных изменений : чтобы внести небольшие и безопасные изменения, нам часто нужно разбивать их и постоянно развивать код в направлении целевого проекта посредством серии небольших изменений, каждое из которых находится в ограниченной области кодовой базы и вдоль одной оси, изменяя только одну вещь. Чем меньше и безопаснее изменения, тем быстрее мы сможем их выполнить и проверить, и, следовательно, со временем довольно сильно их изменить — это то, что Кент Бек называет стратегией многоножки в спринте.

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

Предварительные условия : Конечно, следовать стратегии Спринтинговой многоножки можно только в том случае, если вы можете быстро создавать, тестировать, развертывать и получать предупреждения о возможных проблемах. Чем дольше цикл тестирования и развертывания (и, следовательно, обратной связи), тем больше шагов вы должны сделать, иначе вы будете тратить большую часть времени на ожидание.

часто задаваемые вопросы

Как я могу внедрить в производство изменение, которое я не совсем уверен, правильно ли?

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

Вывод

Может показаться, что медленно следовать стратегии развития программного обеспечения Sprinting Cantipede, то есть выполнять небольшие, безопасные, в основном неразрывные, инкрементальные шаги, регулярно объединяясь с основной веткой разработки и проверяя изменения часто путем запуска тестов и развертывания в производственной среде. , Но наш опыт показал, что «слепой лягушачий прыжок» — крупное изменение или партия изменений, вызванные надеждой, — на самом деле может быть намного медленнее и иногда невозможным из-за неожиданных, но всегда присутствующих осложнений и дефектов и последующих задержек, расходящихся ветвей и т. Д. И я считаю, что это случается довольно часто. Поэтому меняйте только одну вещь за раз, предпочтительно, чтобы изменения могли быть развернуты, не требуя других изменений (конфигурации и т. Д.), Собирать отзывы,убедитесь, что вы можете прекратить рефакторинг в любое время, получая при этом как можно большую ценность (вместо того, чтобы вкладывать большие усилия в большие изменения и рисковать, что они будут полностью отменены). Какой у тебя опыт?

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

Рекомендации

Подтверждение

Я хотел бы поблагодарить моих коллег Андерса, Мортена и Жанин за их помощь и обратную связь. Извините, Андерс, я не могу сделать это короче. Вы знаете, каждое слово — мое дитя :-).