Введение
Статья представляет собой краткий обзор-объяснение того, как наша команда работает с флагами функций (они же фича-флаги, англ. 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
, не задумываясь о всяких “если”, тестах и сложных способах действий.
Заключение
Вы можете указать на то, что такой подход разработки создает много дублирующего и избыточного кода (и вы правы), но это совсем на короткое время. В число преимуществ данного подхода входят:
- упрощенная разработка;
- идеальное разделение двух путей, включая тесты;
- простой и быстрый процесс удаления старого кода;
- сокращение ошибок, вызванных человеческим фактором, при добавлении нового кода или его тестировании.
Намного лучше, чем вставлять флаги и смешивать их с кодом продакшн. Советую попробовать — за загрузку дополнительных строк кода в репозиторий денег не берут!
Читайте также:
- Фича-флаги времени компиляции в Rust: зачем, как и когда используются
- Как реализовать feature gate в React
- Рефакторинг кода Go для тестопригодности: возможности интерфейсов
Читайте нас в Telegram, VK и Дзен
Перевод статьи Felipe Álvarez: How we manage Feature Flags 🚀