Укрощаем строптивый код: Feature Flags в Kotlin проектах

Онлайн-тренажер Python для начинающих

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

Начать курс

Предисловие: Почему фичи ломают релизы?

Каждый разработчик хотя бы раз оказывался в ситуации, когда новая, блестящая функциональность уже готова, но её нельзя выпускать. То ли она не прошла полное QA, то ли маркетинг ещё не подготовил баннеры, то ли просто страшно нажимать кнопку "Deploy" в пятницу вечером. В этот момент мы обычно начинаем писать if, переключать BuildConfig или, что ещё хуже, комментировать код.

Знакомо? Добро пожаловать в мир feature flags (или feature toggles). Это не просто "ещё одна архитектурная штука". Это ваш спасательный круг, который позволяет выкатывать код в production, но включать его только для избранных. В этой статье мы разберём, как внедрить feature flags в Kotlin проектах — от простых решений до продвинутых систем. И да, будет много кода.

Что такое Feature Flag и зачем он вам нужен?

Feature Flag (он же фича-тоггл) — это программный переключатель, который позволяет включать или выключать функциональность без изменения кода и повторного деплоя. Представьте, что вы можете залить новый алгоритм рекомендаций в production, но сделать его активным только для 5% пользователей. Или включить его только для сотрудников компании для внутреннего тестирования. Круто, правда?

Вот несколько сценариев, где feature flags незаменимы:

  • Canary releases: Выкатываете новую фичу на 1% серверов, мониторите ошибки и постепенно увеличиваете процент.
  • A/B тестирование: Проверяете, какая версия UI конвертирует лучше, без копирования всего проекта.
  • Kill switch: Если новая фича начала "жрать" память или вызвала баг, вы просто выключаете её одним движением (или API-запросом).
  • Разделение релиза и деплоя: Код залит, но фича "спит" до даты X.

В Kotlin, который активно используется как для бэкенда (Ktor, Spring Boot), так и для мобильных приложений (Android), feature flags становятся особенно актуальными из-за строгой типизации и требований к чистоте кода.

Архитектура: Как не превратить флаги в спагетти

Самая большая ошибка новичков — разбрасывать if (flag.isEnabled()) { ... } else { ... } по всему проекту. Это приводит к тому, что через месяц вы не сможете понять, какие ветки кода живы, а какие мертвы. Нам нужно управление фичами, а не хаотичные переключатели.

Хорошая архитектура строится на принципе инверсии зависимостей. Ваш бизнес-код не должен знать, откуда берётся значение флага (из файла, базы данных или внешнего сервиса). Он просто спрашивает: "Эй, FeatureFlagService, а фича new_checkout включена?"

Рассмотрим два подхода: простой (для небольших проектов) и продвинутый (для микросервисов и Kubernetes).

1. Простой подход: Конфигурация и Enum

Если вам нужно быстро начать и вы не строите систему с миллионом запросов в секунду, можно обойтись простым enum class и файлом конфигурации (например, application.conf для Ktor или application.yml для Spring).

Создадим enum всех фич:

enum class Feature(val key: String) {
    NEW_PAYMENT_FLOW("feature.new-payment-flow"),
    DARK_MODE("feature.dark-mode"),
    SMART_SEARCH("feature.smart-search")
}

Теперь сервис, который читает конфиг:

class FeatureFlagService(private val config: Map<String, Boolean>) {

    fun isEnabled(feature: Feature): Boolean {
        return config.getOrDefault(feature.key, false)
    }

    // Для A/B тестов можно добавить рандомизацию
    fun isEnabledForUser(feature: Feature, userId: String): Boolean {
        if (!isEnabled(feature)) return false
        // Простейший хеш для разделения на группы
        val group = userId.hashCode() % 100
        return group < 50 // 50% пользователей
    }
}

Использование в коде:

if (featureFlagService.isEnabled(Feature.NEW_PAYMENT_FLOW)) {
    // Новый поток оплаты
    showNewPaymentScreen()
} else {
    // Старый поток
    showLegacyPaymentScreen()
}

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

2. Продвинутый подход: Внешние сервисы (Unleash / LaunchDarkly)

Для серьёзных проектов, где флаги должны меняться в реальном времени, используются специализированные сервисы. Один из самых популярных — Unleash. Он предоставляет SDK для Kotlin, поддерживает стратегии, A/B тесты и веб-интерфейс.

Подключим его в проект на Ktor:

// build.gradle.kts
implementation("io.getunleash:unleash-client-java:8.0.0") // Java SDK, но отлично работает с Kotlin

Инициализация клиента:

import io.getunleash.Unleash
import io.getunleash.UnleashContext
import io.getunleash.util.UnleashConfig

val config = UnleashConfig.builder()
    .appName("my-kotlin-app")
    .unleashAPI("https://your-unleash-instance.com/api")
    .apiKey("your-api-key")
    .build()

val unleash = Unleash(config)

// Проверка флага
if (unleash.isEnabled("new-checkout")) {
    println("New checkout is active!")
}

Но самое интересное — это контекст. Вы можете передавать информацию о пользователе, сессии и даже окружении:

val context = UnleashContext.builder()
    .userId(user.id.toString())
    .sessionId(sessionId)
    .remoteAddress(clientIp)
    .build()

if (unleash.isEnabled("dark-mode", context)) {
    // Включаем тёмную тему для конкретного юзера
}

В Unleash вы можете настроить стратегии: "Включить для 10% пользователей", "Включить для пользователей из России", "Включить только для сотрудников" — и всё это без перекомпиляции.

Feature Flags в Spring Boot с помощью Togglz

Если вы используете Spring Boot, обратите внимание на библиотеку Togglz. Она интегрируется прямо в Spring Context и позволяет управлять флагами через JMX, REST API или файлы.

Пример конфигурации:

import org.togglz.core.Feature;
import org.togglz.core.annotation.EnabledByDefault;
import org.togglz.core.annotation.Label;

public enum Features implements Feature {
    @Label("Новый поиск")
    @EnabledByDefault
    NEW_SEARCH,

    @Label("Экспериментальный API")
    EXPERIMENTAL_API
}

В Spring-сервисе:

import org.springframework.stereotype.Service
import org.togglz.core                

Блоги

Рекомендаций по книгам