Pull to refresh

Service Locator и Branch By Abstraction — супер-зелье

Reading time 5 min
Views 5K
Сегодня популярен подход в разработке приложения, имя которому 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 в исчадье ада, а процесс релиза помогает держать на очень высокой частоте. Детальнее об этом прочитать можно в хорошой статье тут.

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

Articles