Шаблоны и сценарии для подготовки каждого коммита

Ветвление считается вредным?

Когда я был ребенком, у нас не было филиалов. Ну, вроде как, но вы посадили их в угол и попытались представить, что их не существует. Это были тяжелые, неприятные вещи. Дорогой в создании, сложный в обслуживании, невозможно объединить.

Когда появился Git с его легковесным ветвлением и распределенностью, он решил множество проблем - и создал гораздо больше. Мы формируем наши инструменты, а затем наши инструменты формируют нас.

Мы делаем то, что делаем легко. После того, как ветвление и, что более важно, слияние прошло быстро и безболезненно, мы стали делать это чаще. Функциональные ветки и запросы на вытягивание вошли в общий язык. Создан Gitflow. Я смотрю на эти диаграммы Gitflow и думаю, что мог бы построить домик на дереве в этих ветвях! Но для многих это было улучшением . И если честно, я был одним из них.

Если вы позволите мне немного побаловать себя, давайте соберемся вокруг дерева рассказов, прежде чем мы продолжим собственно статью. Когда я был энергичным молодым инженером-программистом и еще заканчивал учебу, я присоединился к давнему проекту как раз вовремя, чтобы стать частью Великой миграции CVS на SVN. Это было слишком давно, чтобы я мог вспомнить хоть что-нибудь об использовании CVS, но я принимал участие в «процессе выпуска», поскольку он был переписан для SVN.

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

Если бы нам пришлось выбирать фиксацию в ветке исправлений (которая на самом деле была тегом), мы ... я не знаю, могу ли я это признать. Мы ... просто снова написали код. Рукой. Из памяти. Мой большой вклад в улучшение этого процесса заключался в том, что я обнаружил в Eclipse возможность «сравнить со стволом» и научил команду, как ее использовать.

У нас также не было модульных тестов или даже ночных сборок, не говоря уже о CI-сервере. У нас не было флажков функций. Развернулись на стволе. Если код не был готов к фиксации… мы его не фиксировали. Он сидел на наших машинах день за днем, неделя за неделей. Иногда мы копировали наши рабочие каталоги на сетевой диск в качестве страховки от сбоя жесткого диска. В других случаях мы бросали осторожность и просто позволяли ей оставаться на месте.

Затем, когда мы были близки к завершению, началась гонка! Кто-то другой также может быть близок к совершению. Задача заключалась в том, чтобы внести ваши изменения первыми, а остальные оставить с мучительным слиянием. Не привела ли первая спешка к упущениям и глупым ошибкам? да. Да, это так.

Филиалы лучше, чем это, правда? Ну, может быть.

Все контекстно. Я не знаю вашего контекста. Я не знаю ни вас, ни вашу компанию, ни ваших коллег, ни как вы работаете. Но я все равно сделаю заявление. Большое, смелое, потенциально пугающее заявление.

Филиалы сбили вас с пути. Gitflow вам не друг.

Вы должны сразу стать мастером.

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

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

  1. Это может что-то сломать
  2. Не готово
  3. Нам нравится содержать "хозяина" в чистоте и стабильности.
  4. Как мы можем позволить «младшим» вводить код без проверки?
  5. Более чем горстка разработчиков, работающих над одной и той же кодовой базой в одно и то же время, создаст беспорядок.
  6. «Здесь не сработает» - в нас есть что-то особенное, что означает, что CI / CD не подходят.

Когда я обсуждаю удаление функциональных веток с моими коллегами-инженерами, я говорю с умными, хорошо аргументированными людьми. Они видели, как работают ветки, а разработка на основе ствола терпит неудачу. Если возвращаться к жизни без веток опасно и сложно, зачем нам беспокоиться?

Исходя из моего опыта и несмотря на мой первоначальный опыт, у меня есть три основных причины, по которым стоит следовать:

  1. Непрерывная интеграция [CI] и непрерывная доставка / развертывание [CD] имеют хорошо задокументированные, обычно сообщаемые преимущества. Многие из этих преимуществ основаны на том, чтобы сделать цикл дизайн - ›код -› тест - ›выпуск -› измерение как можно короче. Чем дольше вы откладываете нажатие на мастер, тем дольше вы задерживаете какую-то часть цикла обратной связи CI / CD. Обычная реакция на этом этапе - утверждать, что вы можете добавлять master в свою ветку так часто, как захотите! Это правда, но ваш код по-прежнему скрыт от других. Не скрывайте свой код. Это грубо. Нажмите, чтобы освоить.
  2. Оказывается, что ментальные мышцы, которые вам нужно тренировать, когда вы думаете о том, как подтолкнуть, чтобы освоить, не ломая всего, - это те же ментальные мышцы, которые вам нужны для развития распределенной системы. Чем больше вы будете практиковаться в разработке на основе магистрали [TBD], тем лучше вы станете в развитии API и схем данных. TBD поможет вам выиграть в микросервисах. Наоборот. Это изящный небольшой цикл обратной связи, который улучшает цикл код / ​​выпуск / измерение, над которым мы также работаем.
  3. Переключатели функций, которые будут использоваться для безопасного управления TBD, могут отделить «код развертывания» от «функции выпуска». Это дает фантастическое преимущество, позволяя включать функцию «когда бизнес будет готов». Больше нет необходимости согласовывать циклы выпуска. Это, в свою очередь, может иметь большое значение для устранения «кризиса выпуска», который оказывает всестороннее положительное влияние на моральный дух, эффективность и качество.

Откажитесь от ствола, и вы откажетесь от этой экосистемы преимуществ. Долговечные ответвления не так плохи, как котята бонсай, но они близки.

Что касается возражений, перечисленных выше, я лично считаю, что все они сводятся к следующему: Не знаю, как это работает, поэтому, вероятно, это не так. По этому пути легко пройти. Многие советы по внедрению CD и TBD можно резюмировать как просто используйте флаги функций! Легко сказать - сложно освоить. В Интернете можно найти хорошую, даже отличную информацию о работе с функциями-флагами. Но хотя флаги функций - это круто и все такое, смерть от флагов функций - это вещь. Зачем создавать ненужные сложности?

В этой статье я более подробно расскажу о некоторых конкретных методах подготовки каждого коммита.

Дополнения безопасны, удаление требует обдумывания

Показываю ли я свой возраст, когда говорю о битвах с RMI и сериализацией Java? Может быть ... Но, без сомнения, есть те из вас, кто опередит меня своими автокатастрофами CORBA и катастрофами DCOM. Тогда у нас были pickling, WSDL и DTD, теперь у нас есть REST, thrift, Avro, gRPC, protobuf и GraphQL, но это одно и то же. Если вам когда-либо приходилось сталкиваться с обратной совместимостью между API, двоичными форматами или версиями схем, у вас есть преимущество. Вам нужно носить ту же «мыслящую шляпу» для разработки на основе ствола.

Добавление новых полей в класс / запись / «объект передачи данных» [DTO] имеет обратную совместимость. Удалять их нельзя, если мы не можем быть на 100% уверены, что они не используются. Изменение поля - это «удалить, затем добавить» за одну операцию, что означает, что применяются правила «удаления».

Один из ключевых способов сделать небольшие коммиты безопасными для каждой отдельной производственной среды - осознать, что нет никакой разницы между заботой о совместимости с чем-то, что вы сохранили на диске ранее, или более старым API, и совместимостью с кодом из «неполной» функции.

Дополнения безопасны. Модификации на месте небезопасны. Удаление безопасно только в том случае, если вы можете гарантировать, что предмет не используется. Исходя из этого, безопасны следующие основные строительные блоки:

  • Добавление неиспользуемого столбца в таблицу
  • Добавление неиспользуемого поля в DTO
  • Добавление неиспользуемого параметра в метод
  • Добавление неиспользуемого метода в класс
  • Добавление неиспользуемого класса в модуль
  • Добавление неиспользуемого модуля в репозиторий

Обратите внимание, что «неиспользованный» означает неиспользованный производственным кодом. Модульные и интеграционные тесты должны дать нам уверенность в том, что добавляемый код будет работать.

Теперь мы можем использовать эти строительные блоки, чтобы составить несколько общих сценариев для добавления новых функций. В следующих сценариях предполагается наличие универсального стека со статически типизированным языком, входящими конечными точками на основе RPC, уровнем логики домена, ORM и уровнем реляционного хранилища. Все выбранные сценарии разделены на интерфейсную и внутреннюю части. Это потому, что я ожидаю, что эта граница будет знакома большинству читателей. Сами «игры» в равной степени применимы к любым архитектурным границам или внутри них, например, между двумя внутренними сервисами в архитектуре микросервисов.

Добавление функций

Вверх дном

Сценарий: форма, в которой ранее записывались имя, фамилия и дата рождения, теперь также должна содержать «любимый салатный зеленый цвет».

Последовательность коммитов:

Предположим, что после каждого пункта ниже у вас есть изменение кода, которое можно безопасно зафиксировать и отправить в мастер.

  • Добавить столбец favouriteSaladGreen в таблицу пользователей (допускающий значение NULL)
  • Добавьте поле favouriteSaladGreen к объекту "Пользователь"
  • Добавьте поле favouriteSaladGreen к объекту передачи «UserDTO», который передается между уровнем домена и уровнем сохраняемости.
  • [Если применимо] Добавьте поле favouriteSaladGreen к объекту передачи, который используется для передачи данных между конечной точкой службы и уровнем домена. (Возможно, вы повторно используете UserDTO, указанный выше)
  • Начать отправку поля favouriteSaladGreen из внешнего интерфейса в серверную, если оно предусмотрено.
  • Добавьте поле favouriteSaladGreen в форму пользователя и привяжите ввод к данным, отправляемым на серверную часть.

Работа сделана!

Плюсы:

  • Для простых изменений вы можете избежать сложностей, связанных с добавлением флага функции. (Хотя вам все равно может понадобиться для A / B-тестирования или контролируемого развертывания.)
  • Прямое поступательное развитие

Минусы:

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

Сверху вниз

Сценарий: любимый зеленый цвет слишком ограничивает. Мы хотели бы получить целый "любимый рецепт" салата от наших пользователей. Пользовательский интерфейс для этого будет умеренно сложным, требующим нескольких элементов формы и слишком большого количества CSS. На то, чтобы развиваться и совершенствоваться, может потребоваться время. Но как только интерфейс будет завершен, дополнительные данные будут отправлены в существующую конечную точку.

Последовательность коммитов:

Предположим, что после каждого пункта ниже у вас есть изменение кода, которое можно безопасно зафиксировать и отправить в мастер.

  • Создайте флажок функции для пользовательского предпочтения «запомнить рецепт салата».
  • Добавьте заглушку, чтобы зафиксировать рецепт салата, чтобы он не появился, если флажок функции отключен.
  • Полная разработка внешнего интерфейса [вероятно, потребует нескольких коммитов, но они надежно скрыты за флажком функции].
  • Добавьте saladRecipe к данным, отправляемым в существующую внутреннюю конечную точку. Дополнительные данные на данный момент не используются.
  • На внутренней стороне проанализируйте saladRecipe информацию в приемнике конечной точки (обычно это контроллер REST, но с учетом вашего стека)… и выбросьте ее.
  • Передайте saladRecipe данные из контроллера на уровень «логики домена». Затем либо добавьте любую логику, которая может вам понадобиться для преобразования, либо манипулируйте ею, обязательно протестировав ее… и выбросьте. Помните, что, поскольку пользовательский интерфейс помечен как функция, вы на самом деле не будете получать эти данные в рабочей среде.
  • Как и выше, передайте saladRecipe из уровня домена в уровень сохранения и выбросьте его. (Если вы используете что-то вроде JPA, для этого можно пометить поля как transient)
  • Измените базу данных, чтобы добавить новую структуру таблицы, которая вам нужна.
  • Прекратите выбрасывать saladRecipe данные, теперь их можно сохранить.

Работа сделана! Функция работает сквозная.

Плюсы:

  • Устраняет риск того, что требования к интерфейсу не будут согласованы с реализацией серверной части.
  • Прямое поступательное развитие

Минусы:

  • Требуется флаг функции, что является дополнительной сложностью, поэтому перед внедрением подумайте, достаточно ли мала функция для разработки «снизу вверх».

Середина

Сценарий. Записав «любимый рецепт салата», мы хотим создать функцию, которая позволит подключенным пользователям видеть рецепты друг друга и формировать мнение о том, достаточно ли в их рационе суперпродуктов.

Процесс разработки. Хотя описанный сценарий имеет много осложняющих факторов, мы могли бы наивно свести его к разделению на интерфейсные и внутренние части. Лично я обычно высмеиваю такую ​​ленивую разбивку и всегда предпочитаю искать вертикальные срезы ценности. Но давайте предположим, что мы попробовали это и уже сократили нашу задачу до чего-то, что требует значительного объема работы по обе стороны границы обслуживания.

Мы могли бы реализовать эти задачи, используя подход «сверху вниз», который будет иметь то преимущество, что когда мы приступим к созданию нашего API, все требования потребителя будут учтены.

Но что, если мы не сможем этого сделать? Что делать, если вы хотите распараллелить работу?

В прошлом я видел, как эти задачи решались как «наведение моста». Интерфейсная работа осуществляется «сверху вниз». Она начинается с создания компонентов или макетов пользовательского интерфейса с фиктивными данными, которые затем определяют дизайн API. Тем временем начинается внутренняя работа и, не имея определенного API, начинается работа «снизу вверх». Внутренняя работа начинается с ожидаемых изменений данных и продвигается вверх, чтобы раскрыть то, что необходимо в API. Затем вы пытаетесь встретиться посередине.

Это никогда не работает. При достаточном планировании того, как этот API должен выглядеть, вы можете минимизировать ущерб, но, говоря лично здесь, я никогда не видел, чтобы это выполнялось без заминки. В этот момент вы, вероятно, думаете: «Чувство! GraphQL! Планирование? Просто поговорите друг с другом… »И совершенно точно! Вот как мы будем играть в эту игру. Но давайте сделаем еще один шаг. Не просто говорите и определяйте API. Построить это. Первый. Вот что я имею в виду под «средней ценой».

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

Затем интерфейсная часть строится на основе API. Именно на этом этапе мы представляем функцию-флаг. Серверная часть строится от API и ниже. Вполне вероятно, что обе стороны могут обнаружить, что им необходимо внести небольшие изменения в API в процессе работы. Если вы используете протокол RPC со схемой, такой как GraphQL или protobuf, вам может показаться, что этого достаточно, чтобы поддерживать согласованность. В противном случае тесты контрактов, ориентированные на потребителя, такие как Pact или Spring Cloud Contracts, являются отличными инструментами, которые можно использовать здесь. Они выходят за рамки простого согласования API и проверяют во время сборки, что обе стороны соблюдают контракт.

Изменение существующих функций

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

Как мы подходим к изменению существующей функции, зависит от того, нужно ли изменить поведение или нет.

Если еще

Изменение поведения функции аналогично изменению поля в сериализуемом объекте или API. Что касается совместимости версий, то на самом деле выполняется «удаление», а затем «добавление» за один атомарный шаг. Для крошечного изменения, за одну фиксацию - это нормально.

Для длительного изменения в нескольких коммитах нам нужно разбить это на три этапа.

  1. Добавить новое поведение
  2. Используйте новое поведение
  3. Удалите старое поведение

Для реализации первого шага выберите любой из подходящих методов из руководств в предыдущем разделе.

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

if (flagEnabled)
   newBehaviour()
else
   oldBehaviour()

В зависимости от сложности того, что входит в перегруженный термин «поведение», и возможностей, предоставляемых вашим языком, это может стать оператором переключения или шаблоном стратегии. В любом случае - мы разветвляемся в коде, а не в кодовой базе. До определенной степени это проще и легче следовать… До определенной степени. Итак, шаг 3, удалите старое поведение, действительно важен.

После реализации «шага 2» вы можете выполнить A / B-тестирование, и ваша функция может быть представлена ​​клиентам. Если потребность в доставке высока, может возникнуть соблазн положить конец этому моменту. Этого делать нельзя. Функция не будет завершена до тех пор, пока не будет снят флажок. Измерьте использование, объявите об успехе, снимите флаг. Или объявите отказ и удалите функцию. В зависимости от того, как работает команда, в которой вы работаете - оставьте заявку открытой, оставьте карточку на доске или просто добавьте новую карточку / заявку, чтобы убрать ветку внизу вашей очереди, когда вы приступите к работе. Как бы вы это ни делали, убедитесь, что это сделано.

Замена существующих функций

Другая основная причина, по которой вы можете захотеть прикоснуться к существующим функциям, - это не изменение поведения, а улучшение нефункциональных требований, таких как производительность, или замена интегрированной технологии другим вариантом, например замена Clickhouse на Druid.

Ветвь по абстракции

«Переход по абстракции» - не новое понятие. Сама фраза датируется 2007 годом, а сама техника намного старше. Насколько старше сложно сказать, но нам это было нужно еще до появления Git и дешевого ветвления. Интересно, насколько эффективный TBD на самом деле просто заново открывает утерянное искусство?

Многие другие светила программной инженерии обсуждали эту отрасль с помощью техники абстракции, например Мартин Фаулер, Jez Humble и Пол Хаммант. Некоторые из этих описаний труднее переварить, чем другие. Я рекомендую книгу Мартина Фаулера для легкого чтения.

Техника сводится к созданию адаптера или другого подходящего слоя абстракции перед тем, что вы хотите заменить. Направьте All The Things на ваш новый слой абстракции. Поместите переключатель функций внутри слоя абстракции. Когда переключатель щелкает, срабатывает новое поведение. Et voila! Вы поменяли двигатель на своем реактивном самолете, пока он находился в воздухе.

Предостережение: не каждое изменение может быть связано с абстракцией. Иногда вещь, которую вы хотите заменить, слишком сильно переплетена и слишком глубоко связана. Примером этого из прошлого опыта было обновление со Spring 3 до Spring 4 на сложной монолитной кодовой базе. Две платформы не могут одновременно сосуществовать в одном проекте. Использовалась ветка. Это нормально. Вы не можете выиграть их всех.

Ученый

Scientist - это библиотека Ruby для тщательного рефакторинга критических путей - цитируя их собственную страницу GitHub. Он имеет порты на многие другие языки ... и Perl. В отличие от ветвления за абстракцией, методика ученого предполагает запуск как старого, так и нового кодовых путей одновременно и создание отчетов о результатах.

Библиотека родилась в результате рефакторинга GitHub функции «слияния», и любая моя собственная попытка подвести итоги была бы несправедливой. Я предлагаю лучше прочитать исходную статью (ссылка на которую приведена в конце этого раздела), чтобы получить представление о том, что такое библиотека и как она использовалась.

Все, что я добавлю к их описанию, - это чтобы привлечь внимание к первой половине моего собственного сообщения. Добавление неполных функций, которые не используются, безвредно и не требует ветки функций. Новый путь кода может быть построен по частям на мастере. Когда вы подумаете, что он готов, Scientist завоюет доверие к нему.



Слово о тестировании

Несмотря на учебники, несмотря на шаблоны, несмотря на флаги функций и ветвление по абстракции - некоторые изменения по своей сути более рискованны, чем другие. Действительно ли ваш флаг функции работает? Все, что должно быть за этим, на самом деле стоит за этим. Были ли внесены изменения в ваш стиль интерфейса в достаточной мере?

Нам нужна уверенность в том, что изменения, которые мы собираемся осуществить, ничего не сломают. Частью этого является знание того, как структурировать изменения и код для снижения риска, что было основной темой данной статьи. Но только часть.

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

Для особо рискованных изменений полезно иметь возможность как можно раньше провести ручное или исследовательское тестирование в производственной среде. На одном рабочем месте у нас были тестовые среды, которые сбрасывались для отслеживания мастера каждую неделю или около того, но мы могли выполнять специальные развертывания локального кода еще до того, как начнем. Для наиболее рискованных изменений, когда мы были обеспокоены тем, что несколько флагов функций могут плохо взаимодействовать или хотели быть на 110% уверены, что недавно представленный блок кода был помечен, мы собирались локально и тестировали перед отправкой.

На другом рабочем месте у нас были все усилия, чтобы справиться с запуском развертывания в промежуточной среде, за которым следовала гамма «дымовых тестов», чтобы убедиться, что система в основном работает. Что касается изменений, в которых мы были достаточно уверены, мы могли запустить их, чтобы освоить, а затем протестировать на «постановке». У нас также были докеризованы все наши сервисы и настроен конвейер CI, чтобы помечать образы докеров именем ветки. Мы могли развертывать произвольные теги наших сервисов докеров в личных тестовых средах. Хотя функциональные ветки не поощрялись, мы сохранили возможность использовать краткосрочную ветвь для тестирования потенциально опасного изменения в нашей собственной среде, прежде чем переходить к мастеру.

Было ли хорошей идеей тестирование разветвленных изображений? Это, безусловно, дало нам возможность обеспечить себе дополнительный уровень комфорта и уверенности. Возможно, было бы лучше инвестировать в автоматизированные тесты, полагаться на «постановку» как на критерий качества и попытаться внедрить культуру «давай, но возвращайся, если постановка сломается»?

Я не уверен… Но я могу с уверенностью сказать, что подходящая стратегия тестирования должна быть частью любого плана по внедрению или продвижению разработки на основе магистрали.

Заключить

  • Дополнения безопасны, удаления - нет.
  • Большие задачи можно разбить на маленькие.
  • Маленькие задачи можно разделить на последовательность добавления, затем удаления.
  • Флаги функций могут быть разумно вставлены, чтобы заставить логику течь по «старому» пути, пока добавляются, ошибочно, добавления.

Все это звучит довольно сложно. В конце концов, это была длинная статья. Дольше, если вы включите биты, в которых я сделал короткое замыкание и просто указал ссылку на чей-то пост. Если вы все еще спрашиваете: «Почему бы просто не ветвиться?» это законный вопрос. В частности, если вы уже работаете "нормально" с моделью ветвления.

Вернемся к трем моментам, поднятым в самом начале. Магистральная разработка:

  1. Обеспечивает непрерывную интеграцию вашего кода, снижая риски и улучшая цикл обратной связи «код, тестирование, развертывание».
  2. Улучшает вашу способность рассуждать об обратной и прямой совместимости, что приводит к более безопасной и надежной кодовой базе.
  3. Заставляет вас задуматься о введении флагов функций, которые отделяют развертывание от выпуска и освобождают вашу компанию от планирования всего, связанного с датами развертывания.

Это требует практики, приверженности и полностью меняет ваш подход к решению инженерных проблем и написанию кода. К лучшему.