Noveo

Наш блог Подходы к управлению ветками в системах контроля версий (часть 2)

Подходы к управлению ветками в системах контроля версий (часть 2)

Продолжаем знакомить вас с фундаментальным трудом Мартина Фаулера об управлении ветками. В первой части рассматривались базовые паттерны, а во второй части речь пойдет об интеграционных паттернах.

Интеграционные паттерны

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

Интеграция с mainline

Разработчики интегрируют свою работу, выгружая код из mainline, добавляя свои изменения, и — если все прошло успешно — пуша их обратно в mainline.

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

Приведу пример того, как это работает. Разработчик, которого я назову Аллой, начинает работу с клонирования основной ветки в собственный репозиторий. В git’е это будет клонирование центрального репозитория  (если она еще этого не сделала) и переключение на мастер-ветку. Если репозиторий уже склонирован, ей нужно получить изменения из mainline и влить их в ее локальную мастер-ветку. Затем она может работать локально, делая коммиты в свой локальный мастер.

Martin Fowler Source Code Management PatternsПока она работает, ее коллега Виолетта вносит в основную ветку некоторые изменения. Так как Алла работает в своем собственном локальном репозитории над своей задачей, она может не знать об этих изменениях.

Martin Fowler Source Code Management Patterns

Через какое-то время она хочет произвести интеграцию. Первый шаг заключается в том, чтобы синхронизовать ее локальную мастер-ветку с актуальным состоянием mainline, «подтянув» изменения, сделанные Виолеттой. Поскольку она работает в локальном мастере, на origin/master коммиты будут показываться как отдельное состояние кода.

Martin Fowler Source Code Management Patterns

Теперь ей нужно совместить свои изменения с изменениями Виолетты. Некоторые команды любят делать это через слияние (merge), другие — с помощью rebase. Обычно, говоря об объединении веток, люди используют слово «слияние» («merge»), независимо от того, используют ли они на самом деле git-merge или операцию rebase. Я тоже буду придерживаться этого словоупотребления, так что, если только я не говорю о различиях между собственно merge и rebase, считайте «слияние» отдельной задачей, которая может быть реализована с помощью любого из этих способов.

По поводу того, использовать ли vanilla merge, применять ускоренные слияния (fast-forward merges) или избегать их, обращаться ли к операции rebase, существует отдельный спор, выходящий за рамки этой статьи.

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

Martin Fowler Source Code Management Patterns

На этом этапе Алле необходимо проверить, что смерженный код соответствует стандартам качества mainline (предполагая, что mainline является здоровой веткой). Обычно это означает сборку кода и выполнение случайных тестов для mainline. Она должна сделать это, даже если это чистый мерж, потому что даже он может скрывать семантические конфликты. Любые сбои в коммите будут обусловлены только мержем, так как оба «родителя» мержа были «зелёными». Знание этого должно помочь ей отследить проблему, так как подсказку можно будет найти в различиях.

На данный момент она успешно синхронизировала свой код с основной веткой, но — и это важно, хотя часто упускается из виду — еще не закончила интеграцию с ней. Для завершения интеграции ей нужно запушить свои изменения в mainline. Если она этого не сделает, ее изменения будут изолированы от всех остальных в команде — по сути, интеграции не произойдет. Интеграция — и получение изменений, и их отправка (pull and push): только после того, как Алла сделает пуш, ее работа интегрируется с остальной частью проекта.

Martin Fowler Source Code Management PatternsВ наши дни у многих команд перед добавлением коммита в mainline обязательным является шаг code review — подход, который я называю «проверенными коммитами» (Reviewed Commits) и рассмотрю ниже.

Иногда кто-то другой интегрируется с mainline до того, как Алла может сделать свой пуш. В этом случае нужно снова провести синхронизацию и слияние. Обычно это лишь временная проблема, и она может быть решена без какой-либо дальнейшей координации. Я видел, как команды с долгими сборками использовали «эстафетную палочку интеграции» (integration baton): только разработчик, держащий палочку, мог интегрироваться. Но в последние годы билды собираются быстрее, и я не часто слышал об этом способе.

Когда это использовать

Как понятно из названия, я могу использовать интеграцию с mainline, только если мы используем mainline в нашем продукте.

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

Когда люди используют слово «интегрировать», они часто пропускают этот важный момент. Часто можно услышать, как кто-то говорит, что они интегрируют mainline в свою ветку, когда они просто вытягивают к себе изменения. Я научился воспринимать это настороженно и проверять, имеют ли они в виду просто «спуллил» (pulled) или все же интеграцию с mainline. Последствия этих двух вариантов очень разные, поэтому важно не путать термины.

Другая альтернатива — это когда Алла занимается какой-то работой, которая еще не готова к полной интеграции с остальной командой, но пересекается с работой Виолетты, и Скарлетт хочет  этим с ней поделиться. В этом случае они могут открыть ветку для совместной работы (Collaboration Branch).

Отдельная ветка для каждой фичи

Разместите всю работу над фичей в собственной ветке и интегрируйте ее с mainline, только когда фича будет готова.

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

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

Martin Fowler Source Code Management Patterns

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

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

Martin Fowler Source Code Management Patterns

Обратите внимание: это не интеграция, как я описывал выше, так как Алла не пушит свои изменения в mainline. Пока только она видит свою работу, другие нет.

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

Когда она закончит работу над фичей, она выполнит интеграцию с mainline, чтобы включить фичу в продукт.

Martin Fowler Source Code Management Patterns

Если Скарлетт работает над несколькими фичами одновременно, она создаст для каждой из них отдельную ветку.

Когда это использовать

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

Частота интеграции

То, как часто мы делаем интеграцию, оказывает удивительно сильное влияние на то, как работает команда. Исследование из отчета State Of Dev Ops показало, что элитные команды разработчиков интегрируются заметно чаще, чем команды с низкими показателями — это наблюдение соответствует моему опыту и опыту многих моих коллег по отрасли. Я проиллюстрирую, как это происходит, рассмотрев два примера частоты интеграции с участием Аллы и Виолетты.

Редкая интеграция

Начну с низкой частоты. Здесь две наших героини начинают работу с клонирования mainline в свои ветки, затем делают пару локальных коммитов, которые они пока не хотят пушить.

Martin Fowler Source Code Management Patterns

Пока они работают, кто-то еще добавляет коммит в mainline (не могу быстро придумать имя этого человека, отражающее цвет, — может быть, назовем его Серёжей?)

Martin Fowler Source Code Management Patterns

Эта команда работает, сохраняя здоровую ветку и синхронизируясь с mainline после каждого коммита. Алле незачем было синхронизироваться после первых двух коммитов, так как mainline оставалась неизменной, но теперь ей нужно выгрузить себе M1.

Martin Fowler Source Code Management Patterns

Я отметил слияние желтым. Коммиты  S1…3 сливаются здесь с M1. Виолетте скоро нужно будет сделать то же самое.

Martin Fowler Source Code Management Patterns

Сейчас обе разработчицы синхронизированы с mainline, но они не интегрировались, так как изолированы друг от друга. Алла не знает о каких-либо изменениях, внесенных Виолеттой в V1…3.

Алла делает еще пару локальных коммитов, после чего готова к интеграции с mainline. Ей будет легко запушить свой код, так как она до этого выгрузила себе M1.

Martin Fowler Source Code Management Patterns

А вот Виолетте будет сложнее. Когда она делает интеграцию с основной линией, она должна интегрировать S1…5 с V1…6.

Martin Fowler Source Code Management Patterns

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

Частая интеграция

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

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

Martin Fowler Source Code Management Patterns

Первый коммит Аллы также интегрируется в основную ветку, но из-за того, что Виолетта добралась туда первой, ей нужно сделать мерж. Правда, так как она объединяет только V1 с S1, мерж небольшой.

Martin Fowler Source Code Management Patterns

Следующая интеграция Аллы — это простой пуш, а значит, следующий коммит Виолетты также потребует мержа с двумя последними коммитами Аллы. Тем не менее, это все еще довольно мелкий мерж — один коммит Виолетты и два коммита Аллы.

Martin Fowler Source Code Management Patterns

Когда в основной ветке появляется какой-то внешний пуш, он в обычном ритме интеграции подхватывается Аллой и Виолеттой.

Martin Fowler Source Code Management Patterns

Хотя это похоже на то, что происходило раньше, интеграции мельче. На этот раз Алле нужно интегрировать только S3 с M1, потому что S1 и S2 уже были в основной ветке. Это означает, что Сергею пришлось бы интегрировать то, что уже было на основной линии (S1…2, V1…2), прежде чем запушить M1.

Разработчики продолжают работу, интегрируясь с каждым коммитом.

Martin Fowler Source Code Management Patterns

Сравниваем частоту интеграции

Посмотрим еще раз на две обобщающие картинки:

Редкая интеграция

Martin Fowler Source Code Management Patterns

Частая интеграция

Martin Fowler Source Code Management Patterns

Страх интеграции

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

Самым очевидным из них является то, что команда реже выполняет интеграцию, а это приводит к более неприятным инцидентам при мерже, что, в свою очередь, ведет к менее частым интеграциям… и так далее.

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

Парадоксальный выход из ситуации отлично выражается слоганом «если тяжело… делай это чаще».

Здесь есть два бросающихся в глаза различия. Во-первых, частая интеграция, как следует из названия, имеет гораздо больше интеграций — в два раза больше только в этом игрушечном примере. Но, что важнее, эти интеграции намного мельче, чем при редкой интеграции. Более мелкие интеграции означают меньше работы, поскольку код претерпевает меньше изменений, которые могли бы таить в себе конфликты. Более того, это не просто меньше работы, это также меньше риска. Проблема больших мержей заключается не столько в работе с ними, сколько в неопределенности этой работы. В большинстве случаев даже большие мержи проходят гладко, но иногда они происходят очень, очень плохо. И эта случайная боль хуже, чем обычная. Если сравнить дополнительные 10 минут на интеграцию с 1 из 50 шансов потратить 6 часов на исправление ошибок в интеграции — что бы я предпочел? Если я смотрю только на приложенные усилия, то 1 к 50 лучше, ведь это 6 часов, а не 8 часов и 20 минут. Но неопределенность делает случай  1 к 50 гораздо хуже, та самая неопределенность, которая и приводит к боязни интегрироваться.

Давайте посмотрим на разницу между частотой интеграций с другой стороны. Что произойдет, если у Аллы и Виолетты возникнет конфликт в их первых коммитах? Когда они обнаружат конфликт? При редких интеграциях они не обнаруживают его до последнего мержа Виолетты, потому что только здесь S1 и V1 впервые объединяются. Но при частых интеграциях конфликт обнаруживается при самом первом мерже от Аллы.

Редкая интеграция

Martin Fowler Source Code Management Patterns

Частая интеграция

Martin Fowler Source Code Management Patterns

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

Возможно, глядя на вытавление счета, Виолетта увидела, что он включает в себя оценку налога в тех случаях, когда автор предполагал определенный механизм налогообложения. Ее фича требует другого подхода к налогообложению, поэтому лучшим решением было бы убрать налоги из механизма выставления счета и позже реализовать их как отдельную функцию. К функции выставления счета обращались только в паре мест, поэтому легко использовать паттерн Move Statements to Callers — и результат имеет больший смысл для будущего развития программы. Но Алла не знала, что Виолетта делает это, и написала свою фичу, предполагая, что функция биллинга учитывает налоги.

В таких ситуациях самотестирующийся код — наш спаситель. Если у нас есть хороший комплекс тестов, использование его как части здоровой ветви позволит обнаружить конфликт, так что вероятность того, что ошибка попадет в продакшн, будет гораздо меньше. Но даже если этот набор тестов выступает в качестве привратника на пути к основной ветке, крупные интеграции усложняют жизнь. Чем больше кода нужно интегрировать, тем труднее найти баг. Кроме того, у нас больше шансов обнаружить несколько взаимосвязанных ошибок, которые очень сложно понять. При более мелких коммитах мы не только уменьшаем количество кода, в котором нужно будет искать ошибку, но и можем использовать Diff Debugging для отслеживания изменений, повлекших за собой проблему.

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

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

Непрерывная интеграция (Continuous Integration)

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

После того, как команда убедится, что частая интеграция более эффективна и менее напряжна, естественным вопросом будет: «Как часто мы можем это делать?». Работа над каждой фичей в отдельной ветке подразумевает ограничение по минимуму изменений — их размер не может быть меньше связующей фичи.

Непрерывная интеграция применяет другой триггер для интеграции — вы интегрируетесь всякий раз, когда делаете большой объем работы над фичей и ваша ветка по-прежнему здорова. Не ожидается, что фича будет закончена, важно, что в код было внесено достаточное количество изменений. Правило заключается в том, что «каждый коммитит в основную ветку каждый день», а точнее: в вашем локальном репозитории не должно быть неинтегрированного кода больше, чем за день работы. На практике большинство приверженцев непрерывной интеграции интегрируются много раз в день, с удовольствием интегрируя час работы или меньше.

Разработчикам, использующим непрерывную интеграцию, необходимо привыкнуть к тому, что к моменту интеграции они часто приходят с фичей, реализованной лишь частично, и найти способ интегрироваться, не выставляя полуготовую функциональность в работающую систему. Часто это просто: если я реализую алгоритм скидки, основанный на коде купона, и этот код еще не внесен в список действующих, то мой код не будет вызван, даже если он уже будет на проде. Аналогично, добавляя фичу, которая спрашивает у человека, оформляющего медицинскую страховку, курит ли он, я могу построить и проверить логику кода и убедиться, что он не используется на проде, оставив реализацию UI с этим вопросом на последний день работы над этой фичей. Часто эффективным методом скрытия полуготовой функциональности является подключение Keystone Interface.

Continuous Integration и Trunk-Based Development

Когда в 2000 году в ThoughtWorks начали использовать непрерывную интеграцию, мы написали CruiseControl, «демон», который автоматически собирал продукт после каждого коммита в основную ветку. С тех пор было разработано множество таких инструментов (например, Jenkins, Teamcity, Travis CI, Circle CI, Bamboo и другие). Но большинство организаций, используя эти инструменты, делают это для автоматической сборки фиче-ветки после коммита — что, хотя и полезно, на самом деле означает, что они не практикуют непрерывную интеграцию (правильнее было бы назвать их инструментами непрерывной сборки).

Из-за этой многозначности некоторые люди стали использовать термин «Trunk-Based Development» вместо «Continuous Integration» (некоторые действительно чувствуют тонкое различие между этими двумя терминами, но их использование непоследовательно). И хотя я обычно крайне педантичен в языковых вопросах, я предпочитаю использовать термин «непрерывная интеграция». Отчасти потому, что не считаю попытки постоянно придумывать новые термины жизнеспособным способом борьбы с многозначностью. Однако главная причина, скорее всего, в том, что, на мой взгляд, изменение терминологии грубо стирает вклад пионеров раннего экстремального программирования, в частности Кента Бека, который придумал и четко определил практику непрерывной интеграции в 1990-х годах.

Если нет способа легко скрыть не готовую функцию, мы можем использовать Feature Flags. Они позволяют не только скрыть сырую функциональность, но и выборочно показать ее группе пользователей — часто это удобно для постепенного запуска новой фичи.

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

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

Когда это использовать

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

Сравнивая ветвление по фичам и непрерывную интеграцию

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

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

Как я уже отмечал, чем чаще интеграция, тем она проще, тем меньше страх перед ней. Порой это трудно объяснить. Если вы живете в мире, где интеграции происходят раз в несколько недель или месяцев, то интеграция, скорее всего, является очень рискованным видом деятельности. Трудно будет поверить, что это можно делать много раз в день. Но интеграция — это одна из тех вещей, где частота снижает сложность. Это контринтуитивное понятие: «если тяжело… делай это чаще». Но чем мельче интеграции, тем меньше вероятность того, что они превратятся в эпический мерж страданий и отчаяния. При разветвлении по фичам это аргумент в пользу более мелких фич: дни, а не недели (а уж про месяцы и вовсе лучше забыть).

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

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

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

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

        — State of DevOps Report 2016

Когда я сталкиваюсь с научными исследованиями практик разработки ПО, я обычно остаюсь не убежденным из-за серьезных проблем с их методологией. Одно из исключений  — отчет State Of Dev Ops Report, который разработал метрику эффективности поставки ПО, которую они соотнесли с более широким показателем эффективности работы организации, который, в свою очередь, коррелирует с такими бизнес-показателями, как доходность инвестиций и рентабельность. В 2016 году они впервые провели оценку непрерывной интеграции и пришли к выводу, что она способствовала повышению эффективности разработки ПО, и с тех пор этот вывод повторяется в каждом опросе.

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

Ветвление по фичам Непрерывная интеграция
✅ Качество всего кода фичи может быть оценено как единое целое.

✅ Код фичи добавляется к продукту только тогда, когда она готова.

❌ Более редкие мержи

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

✅ Сокращает время на поиск конфликтов.

✅ Мержи меньше.

✅ Способствует рефакторингу.

❌Требует приверженности здоровым ветвям (и, следовательно, самотестирующемуся коду).

✅ Научно доказано: способствует повышению эффективности поставки ПО.

Feature Branching и Open Source

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

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

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

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

Проверенные коммиты

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

Code review уже давно поощряется как способ повышения качества кода, улучшения модульности, удобочитаемости и устранения дефектов. Несмотря на это, коммерческие организации часто сталкивались с трудностями при внедрении code review в процесс разработки. Однако в мире опенсорса широко распространена идея о том, что контрибьюшны в проект должны проходить ревью, прежде чем принять их в основную ветку проекта, и в последние годы этот подход получил широкое распространение среди организаций, занимающихся разработкой, особенно в Силиконовой долине. Такой рабочий процесс особенно хорошо вписывается в гитхабовский механизм пулл-реквестов.

Такой рабочий процесс начинается, когда Алла заканчивает часть работы, которую она хочет интегрировать. Поскольку она интегрируется с основной веткой (конечно, если в ее команде есть такая практика) после успешной сборки, но перед тем, как осуществить пуш, она посылает свой коммит на ревью. Другой член команды, например, Виолетта, делает code review. Если ей что-то не нравится, она оставляет комментарии, возвращает его Алле, та дорабатывает, отсылает обратно, и так до тех пор, пока они обе не будут удовлетворены.  Только после этого коммит идет в основную ветку.

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

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

Когда это использовать

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

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

Code review не обязательно делается до того, как код попадет в основную ветку. Многие техлиды считают полезным просматривать код после коммита, сообщая разработчикам о проблемных местах. Культура рефакторинга стоит усилий, нужных на ее формирование: если все сделать как надо, создается сообщество, где каждый член команды регулярно ревьюит код и исправляет найденные проблемы.

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

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

Конфликты при интеграции

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

Ручной процесс здесь является известным источником конфликтов, особенно если он предполагает координацию с отдельными организациями. Конфликты такого типа часто можно сократить с помощью автоматизированных процессов, повышения уровня образования разработчиков (устранение необходимости ручного процесса интеграции с помощью автоматизированных) и продвижения шагов к более поздним этапам Deployment Pipeline или QA в проде. Вы можете найти больше идей для уменьшения таких конфликтов в материале о непрерывной интеграции и непрерывной поставке. Конфликты этого типа также возникают на пути к проде, с теми же самыми трудностями и способами их устранения.

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

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

Важность модульности

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

Модульность также влияет на интеграцию. Если в системе хорошие модули, то большую часть времени Алла и Виолетта будут работать в хорошо разделенных частях кода, и их изменения не приведут к конфликтам. Хорошая модульность также улучшает такие техники, как Keystone Interface и Branch By Abstraction, без необходимости уходить в изоляцию веток. Часто команды вынуждены использовать ветки, потому что отсутствие модульности лишает их других возможностей.

Feature Branching — это модульная архитектура бедняка; вместо того, чтобы создавать системы с возможностью легкой замены функций во время деплоя или даже во время работы приложения они приковывают себя к управлению исходным кодом, обеспечивая этот механизм посредством мержа вручную.

— Дэн Бодарт (Dan Bodart)

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

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

Мои мысли о подходах к интеграции

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

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

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

Оригинал: https://martinfowler.com/articles/branching-patterns.html#BasePatterns

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

НазадПредыдущий пост ВпередСледующий пост

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: