Как эффективно использовать флаги функций

Введение 

Статья представляет собой краткий обзор-объяснение того, как наша команда работает с флагами функций (они же фича-флаги, англ. feature flags, сокр. FF). Здесь мы рассмотрим, как добавлять их в код и тесты, а потом удалять самым простейшим образом во избежание ошибок. 

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

Применение FF проиллюстрируем на примерах упрощенного кода (в реальном мире мы проходим ряд промежуточных этапов из-за использования TDD).

Описание задачи 

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

Например, изменим фильтр в одном из действий, который только извлекает информацию из базы данных. Поехали!

Действие 

Действие выглядит так: 

class RetrieveStuff:
def execute(self, name: str) -> Stuff:
return Stuff.objects.filter(name=name).first()

Сначала меняем имя, присоединяя суффикс Old, после чего добавляем новое действие: 

class RetrieveStuffOld:
def execute(self, name: str) -> Stuff:
return Stuff.objects.filter(name=name).first()

class RetrieveStuff:
def execute(self, name: str) -> Stuff:
return Stuff.objects.filter(name=name, is_active=True).first()

Тесты для действия 

Применяем тот же подход, что и в случае с действием. Изначально у нас есть тесты (с уже измененным именем): 

class TestRetrieveStuffOld:
def test_retrieve_stuff_by_name(self):
Stuff.objects.create(name="a_name")

stuff: Stuff = RetrieveStuffOld().execute("a_name")

assert stuff.name == "a_name"

Теперь мы дублируем тесты для нового действия и вносим необходимые изменения: 

class TestRetrieveStuffOld:
def test_retrieve_stuff_by_name(self):
Stuff.objects.create(name="a_name", is_active=False)

stuff: Stuff = RetrieveStuffOld().execute("a_name")

assert stuff.name == "a_name"


class TestRetrieveStuff:

def test_retrieve_stuff_by_name_and_active(self):
Stuff.objects.create(name="a_name", is_active=True)

stuff: Stuff = RetrieveStuff().execute("a_name")

assert stuff.name == "a_name"
assert stuff.is_active

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

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

class RetrieveStuffView(APIView):
def get(self, name: str):
stuff: Stuff = RetrieveStuffOld().execute(name)

return Response(data=StuffSerializer(stuff).data)

На этом этапе мы обычно создаем пул-реквест и и развертываем его в продакшн. 

Поэтому самое время добавить FF в представление:

class RetrieveStuffView(APIView):
def get(self, name: str):
if not flags.is_active("use-new-filter-for-stuff"):
stuff: Stuff = RetrieveStuffOld().execute(name)

return Response(data=StuffSerializer(stuff).data)

stuff: Stuff = RetrieveStuff().execute(name)

return Response(data=StuffSerializer(stuff).data)

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

Тесты для представления 

Действуем практически так же, как и в случае с тестами для действия. Мы дублируем имеющиеся тесты для представления и добавляем фикстуру fixture, чтобы включать или выключать FF для каждого тестового класса: 

class TestRetrieveStuffViewWhenFlagDisabled:

@pytest.fixture(autouse=True)
def manage_flag(self, activate_flag):
activate_flag("use-new-filter-for-stuff", False)

def test_retrieve_stuff_by_name(self, client):
response = client.get("/api/stuff/a_name/")

assert response.status_code == 200
assert response.json() == {
"name": "a_name",
"is_active": False,
}



class TestRetrieveStuffView:

@pytest.fixture(autouse=True)
def manage_flag(self, activate_flag):
activate_flag("use-new-filter-for-stuff", True)

def test_retrieve_stuff_by_name_and_active(self, client):
response = client.get("/api/stuff/a_name/")

assert response.status_code == 200
assert response.json() == {
"name": "a_name",
"is_active": True,
}

Отлично! Теперь мы можем сделать еще один пул-реквест с этим кодом, создать и активировать новый FF в системе и начать использовать новую функциональность в среде продакшн. 

Удаление флага функций

Переходим к самому интересному этапу. Как правило, мы удаляем FF через 2 дня после развертывания нового кода в продакшн (или после того, как посчитаем, что новый код прошел 100% проверку).

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

Представление
Действие 
Тесты для представления 
Тесты для действия 

Всего лишь нужно удалить все с суффиксом Old или WhenFlagDisabled, не задумываясь о всяких “если”, тестах и сложных способах действий.  

Заключение 

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

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

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Felipe Álvarez: How we manage Feature Flags 🚀

Предыдущая статья7 признаков того, что вы стали продвинутым пользователем Sklearn
Следующая статьяРисуем Дораэмона с помощью Python