Статьи

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

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

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

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

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

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

Большой неудачный рефакторинг

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

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

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

Лучше?

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

Введите экземпляр конфигурации

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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 — он медленнее, но окупается благодаря тому, что вы не тратите время на отладку и устранение неполадок, связанных с производством. (Показывать время, которое вы * не * теряете в своей команде или руководстве, к сожалению, сложно.)

Спринт Сороконожка — серия крошечных изменений

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

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

Prerequisities

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

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

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

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

Вывод

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

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

Ресурсы

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

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

Ссылка: Стратегия Sprinting Centipede: как улучшить программное обеспечение, не ломая его, от нашего партнера JCG Якуба Холи в блоге Holy Java .