Сегодня популярен подход в разработке приложения, имя которому Git Workflow. Бывает доходит до такого, что на вопрос, используете ли вы данный подход, удивленно отвечают: «а кто его не использует?». На первый взгляд — это действительно удобная модель, простая и понятная, но тот, кто ведет разработку с большим количеством участников знает, насколько сложны и утомительны порой бывают мерджи. Какие тяжелые конфликты могут возникнуть и какая рутинная работа их решать. Уже в команде из двух разработчиков можно услышать вздохи о предстоящем слиянии, что говорить про 10, 20 разработчиков? Плюс зачастую есть три основные ветки — (условно) dev, staging, prod — которые тоже кто-то должен поддерживать в актуальном состоянии, тестировать и решать конфликты слияний. Причем не только в одну сторону, а и в обратную, ведь если на продакшне оказывается баг и срочно нужно что-то предпринимать, то нередко хотфикс уходит в продакшн, а потом мерджится в другие ветки. Конечно, если тим-лид или другой счастливец, ответственный за выкладку, — полу-робот, то проблема раздута. Но если есть желание попробовать другой вариант разработки, то под катом есть предложение супер-зелья.



Составляющие


Итак, два паттерна — Service Locator и Branch By Abstraction — это ингредиенты для готовки нашего супер-зелья. Буду ориентироваться на то, что читатель знаком с Service Locator, если же нет — вот статья Мартина Фаулера. Также в интернете много литературы об этом паттерне, как с позитивными, так и с негативными оттенками. Кто-то его вообще называет антипаттерном. Можете еще статью «за и против» на хабре прочитать. Мое мнение — это очень удачный и удобный паттерн, который надо знать, как использовать, дабы не переборщить. Собственно, как и все в нашем мире — находите золотую середину.

Итак, второй компонент — это Branch By Abstraction. По ссылке опять отправляю заинтересовавшихся к Фаулеру, а кому лень — опишу вкратце суть здесь.

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

Магия


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

interface IDo
{
    public function doBaz();
    public function doBar();
}

class Foo implements IDo
{
    public function doBaz() { /* do smth */ }
    public function doBar() { /* do smth */ }
}

class Baz implements IBaz
{
    public function __construct(IDo $class) {}
}

Появляется задача изменить работу doBaz() и добавить новый метод doGood(). Добавляем в интерфейс новый метод, также делаем заглушку в классе Foo и создаем новый класс рядом со старым:

class FooFeature implements IDo
{
    public function doBaz() { /* new code */ }
    public function doBar() { /* do smth */ }
    public function doGood() { /* do very good  */ }
}

Отлично, но как теперь мы сделаем «включатель фичи» и будем внедрять новый класс в клиентский код? В этом поможет Service Locator.

File service.php
if ($config->enableFooFeature) { // здесь может быть любое условие: GET param, rand(), и т.п.
    $serviceLocator->set('foo', new FooFeature)
} else {
    $serviceLocator->set('foo', new Foo)
}
$serviceLocator->set('baz', new Baz($serviceLocator->get('foo')));

Класс Baz имеет зависимость от Foo. Service Locator сам инжектит требуемую зависимость, разработчику нужно только получить класс из локатора $serviceLocator->get('baz');

И в чем супер-сила?


Замена старого класса на новый происходит в одном месте и по всему приложению, где используется локатор. Мысленно можно представить, что больше не нужно искать по всему проекту new Foo, Foo::doSmth(), чтобы заменить один класс на другой.
Условие, по которому будет попадать по ключу в локатор тот или иной класс, может быть каким угодно — настройка в конфиге, зависящая от окружения (dev, production), GET параметр, rand(), время и так далее.

Такая гибкость и позволяет вести разработку в одной ветке, которая является dev и prod одновременно. Нету никаких слияний и конфликтов, разработчики безбоязненно пушат в репозиторий, потому что в конфиге на продакшене новая фича выключена. Функционал, который разрабатывается, виден для других разработчиков. Есть возможность протестировать на боевом продакшене, как себя ведет новый код на определенном проценте пользователей или включить только для пользователей с определенными cookies. Условие включения/выключения нового функционала ограничено только фантазией. Можно проверить хитроумную оптимизацию и быстро убедиться, стоит ли ей пользоваться, добавит ли она выигрыша в производительности и на сколько. Если окажется, что новый класс ни в чем не выигрывает старому — просто удалите и забудьте о нем.

А если внезапно оказалось, что новая фича на продакшене имеет баги, то не нужно судорожно откатываться или сломя голову писать хотфикс — достаточно отключить условие ее добавления в локатор и вернуть включение стабильного кода для пользователей, а для разработчиков включить профилировщик, исправить проблему и закомитить фикс без всяких cherry-pick. Трястись перед релизом с таким зельем будете меньше:

image

Когда же новый класс окончательно протестирован, то старый можно полностью удалить, дабы не плодить сущности. Также такая концепция разработки лучше ложится в работу с Continious Integration, если билды собираются из одной ветки. Зеленый билд — продакшн не поломан, можно выкладывать и не надо мерджить ничего или запускать билд на ветке prod. Скорость внедрения нового функционала также вырастает, не случается проблем, что master слишком отстает от dev версии.

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

Негативные последствия


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

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

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

Кто-то этим пользуется?


Да, причем если верить Полу Хаманту, то этот подход практикуется в Facebook и Google и зовется он Trunk Based Development. В своем блоге у него есть много статей на эту тему, если вам интересно почитать — то вот про facebook и google.

Также при разработке Chromium, команда работает с одной веткой trunk и флагами включения/выключения фичей. Так как есть огромное количество всевозможных тестов (12к юнит, 2к интеграционных и т.д.), это не позволяет превратить trunk в исчадье ада, а процесс релиза помогает держать на очень высокой частоте. Детальнее об этом прочитать можно в хорошой статье тут.

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

Комментарии (29)


  1. lair
    24.09.2015 11:23
    +1

    Почему сервис-локатор, а не dependency injection?


    1. PaulBardack
      24.09.2015 11:38
      -1

      Наверное вы имеете в виду dependency injection container, так как di это всего лишь инъекция зависимости. А так, вы можете использовать оба паттерна. Я выбираю сервис-локатор, чтобы в любом месте можно было получить доступ к требуемому объекту, не нагружая конструктор. Хотя с этим можно поспорить.


      1. lair
        24.09.2015 11:42

        Наверное вы имеете в виду dependency injection containe

        Нет, я имею в виду Dependency Injection как паттерн (в противовес Service Locator как паттерну).

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

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

        Ну и да, использовать в сервис-локаторе строковые ключи — плохая идея, в норме используются типы интерфейсов.


        1. PaulBardack
          24.09.2015 11:50
          -1

          Да, понимаю. Скажем так, статье не о пользе/вреде использования di или сервис-локатора. Потому не хотелось бы здесь разводить об этом спор. Если же обойтись без использования сервис локатора, тогда кто будет отвечать за зависимости в коде? С одним di подмена одного класса на другой может привести к изменениям в разных местах, что усложняет, а то и вообще сводит на нет удобство branch by abstraction


          1. lair
            24.09.2015 11:53

            Если же обойтись без использования сервис локатора, тогда кто будет отвечать за зависимости в коде?

            Если заменить сервис-локатор на вбрасывание зависимостей, то за зависимости будет отвечать вбрасывающая система. У Симана все это прекрасно описано.

            С одним di подмена одного класса на другой может привести к изменениям в разных местах

            Да нет же. Вы и так, и так конфигурите некий composition root, задавая соответствие интерфейса и реализации, просто в сервис-локаторе вы потом достаете зависимости сам явно, а в dependency injection их в вас вбрасывает внешняя система.


            1. PaulBardack
              24.09.2015 12:02

              В таком случае — да. Для исключения путаницы, то что вы описали я называю DIC


              1. lair
                24.09.2015 12:04

                Dependency Injection — устоявшийся термин. И для этого не обязательно нужен контейнер.


              1. sAntee
                24.09.2015 23:34

                Какой путаницы? Dependency Injection он и в Африке DI. Можно инжектить в конструктор, а можно в объект. С другой стороны, и Service Locator, и DI являются реализациями Dependency Inversion принципа (последняя буква solid). И вот тут уже путать не надо. DI, в отличае от Service Locator'а, как минимум позволяет а) абстрагироваться от контейнера б) отдать управление жизненым циклом объектов на аутсорс. Если и есть в этом мире антипаттерны, то это классический синглтон, и Service Locator.


  1. tenbits
    24.09.2015 11:53

    А как быть, если разрабатываются две(+n) раздельные фичи для класса `Foo` и в конфигурации у нас свойства: `enableFooFeatureА` и `enableFooFeatureB`?


    1. lair
      24.09.2015 11:56

      Две раздельные фичи в одном классе — нарушение SRP, не делайте так.


      1. tenbits
        24.09.2015 12:05

        ) Да я так и не делаю. Просто ведь из примера — мы добавляем некий функционал к уже существующему классу, то-есть расширяем его. И это вас не смущает? А если мы хотим разбить этот функционал просто на два флага, это уже вас смущает? Или же мы делаем два расширения по очереди, это вас тоже не смущает, а вот уже параллельно, это уже не комильфо?
        Вообщем о single resp. principle можно долго холиварить, но не в этом суть. Суть вопроса, или по данной технике можно лишь одну фичу за раз добавлять в `Foo`.


        1. lair
          24.09.2015 12:09

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

          Можно сколько угодно. Branch By Abstraction — частный случай Feature Hiding, который позволяет регулировать фичи не только путем подмены реализации, но и просто вставкой if-ов в код.


        1. PaulBardack
          24.09.2015 12:10

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


    1. PaulBardack
      24.09.2015 12:06

      Выше уже ответили. Это плохая практика.


      1. tenbits
        24.09.2015 12:10

        > Появляется задача изменить работу doBaz() и добавить новый метод doGood()

        А как быть если для изменения `doBaz` и для нового метода `doGood` нам нужно 2 Флага?


        1. PaulBardack
          24.09.2015 12:11

          Внедрите в таком случае флаг на уровне метода в самом классе.


  1. wheercool
    24.09.2015 13:25

    [offtopic]Прочтение статьи навеяло [/offtopic]


  1. garex
    24.09.2015 14:06

    Мне кажется или кто-то не умеет готовить `git flow` и организовать организацию?

    Настораживает, когда проблемы инфраструктуры решают в коде, а проблемы кода — в триггерах БД ))


    1. PaulBardack
      24.09.2015 14:20

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


      1. garex
        24.09.2015 14:25
        +1

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

        Претензия здесь не к тактике/методу/способу, а к стратегии.

        Ужасы merge'й обычно бывают у веток, которые никто не ребэйзит и у которых миллион «merge feature branch into feature branch from origin:feature branch». Если же всем (контроль — задача лидера) соблюдать банальное правило, что фича-ветка всегда отребейжена на свежий местный develop, то проблем с merge ноль, ибо уже все отребейжено.


        1. PaulBardack
          24.09.2015 14:31

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


          1. garex
            24.09.2015 14:34

            Дык работаете в одной фича-ветке?

            Либо в третью ветку скидываете интерфейс и класс-заглушку (это уже ближе к вашему варианту), но не «засоряете» этим trunk/dev/master/develop/yourname.

            Открою тайну — любая VCS умеет делать ветку от любой другой ветки.


            1. PaulBardack
              24.09.2015 14:38

              Ну вот и начинается. Ветка от ветки, затем оказывается, что это еще нужно кому-то и он еще больше ветвится. А если у вас начинает работать 20 человек, в итоге кол-во веток лично меня начинает страшно напрягать.

              С засорением проекта не спорю — становится много чего. Но зато у каждого есть актуальный код и в тоже время стабильный trunk.


              1. garex
                24.09.2015 14:43

                в итоге кол-во веток лично меня начинает страшно напрягать.


                Дык их публиковать то не надо сразу всем подряд и чистить за собой надо тоже.
                Локально у человека обычно 3 ветки — develop, master, feature/bla.

                А вот вопрос возникает, что когда у нас слишком многим нужно паралелльно, что-то — почему оно так? Low coupling/high cohesion м.б. нужен? Хотя это больше мечты ))

                Приведите реальный пример, когда нескольким людям что-то надо было, м.б. даже с картинкой. Сколько людей зависело? 1/2/4/8/...? Как это оно так получилось?


                1. PaulBardack
                  24.09.2015 14:49

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

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


    1. shuron
      26.09.2015 12:37

      тут вопрос на сколько у вас быстро изменения идут в продакшн.
      Я готово поспорить что те кто постоянно «бранжит» не готовы релизить несколько раз в день в продакшн в принципе.
      А иногда и один раз в день сложно…
      И это леко понять… Мердж — мануальный таск… не автоматизировать.


      1. garex
        26.09.2015 12:43

        Релизить несколько раз в день? Это по-мойму ближе к хотфиксам. Каждый день так делаем, после сложного релиза или когда ВНЕЗАПНО (с) надо что-то кому-то где-то.

        `git flow hotfix start x.y.z`

        За мердж фича-веток надо отпиливать по одному пальцу. Фича-ветки должны только ребейзиться на девелоп — и только после этого вся красота и польза веток начинает быть видна в истории. А когда куча веток начавшихся где-то глубоко внизу под текущей версией девелопа, в которые девелоп ещё и вливается каждый раз… Это уже бессмысленый набор коммитов, а не ветки ((

        По одному пальцу, не меньше.

        PS: shuron, скорее всего у вас тоже организация и дисциплина хромает. Как только мы ввели у себя «китайский» подход — всё сразу наладилось ))


        1. shuron
          27.09.2015 00:24

          2012 Фейсбук уже два раз в день может
          Хотя они лохи.
          Амазон оже 2011-ом кажды 11.6 секунд что-то в продакшн релизил techcrunch.com/2012/08/03/facebook-doubles-release-speed-will-roll-new-code-twice-a-day

          А у нас не все ок, пока но лучше чем год назад, когда я перешел в компанию…

          В моей команде не бранжим с начала проекта, можем релизить и пару раз в день, но в моей команде 3 девелопера и много легаси херни в инфрастктуре что просто не успеваем пока столько сделать для продакшн…
          Баги не редко доходят до продакшн, неплохо перекрыти автоматическими тестами…

          Соседняй команда состоит из 5 девелоперов и они бранжат… Счастье если они раз в неделю релизнутся…


        1. PaulBardack
          27.09.2015 20:26

          Почему может быть только вариант с корявой дисциплиной и кривой организацией?
          Как на счет удобства A/B тестирования на определенном проценте пользователей?
          Про скорость релизов выше тоже привели пример