Как Мы Используем Флаги Функций В Мобильном Приложении Qiwi Кошелек

Привет, Хабр! Меня зовут Василий Материкин, я Android-разработчик в QIWI. В этом посте я расскажу об использовании флагов функций в QIWI Кошельке.



Как мы используем флаги функций в мобильном приложении QIWI Кошелек



Реализация магистральной разработки и флагов функций

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

Мы в QIWI столкнулись с этим два года назад, когда в QIWI Кошельке было создано несколько функциональных команд. Оказалось, что разрабатывать новые фичи с использованием стандартных фиче-веток не так уж и удобно, потому что когда над одним проектом работают несколько фиче-команд, ветки становятся довольно объемными.

Тогда объединить их в мастер становится достаточно сложной задачей и возникают постоянные конфликты.

Поэтому мы решили перейти на Магистральная разработка (TBD).

TBD предлагает работать небольшими ветками и желательно как можно быстрее объединить их с основной веткой.

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

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

Мы выпускаем релизы довольно часто.

И для этого TBD предлагает использовать такие подходы, как ветвление по абстракции (BBA) и флаги функций (FF).

BBA позволяет изолировать любую функциональность в отдельную абстракцию.

Для этого создается интерфейс, описывающий контракт на работу с данным функционалом.

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

Также создается еще одна реализация (с новым функционалом) и с ней начинается работа.

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

При этом проект (в производстве) по-прежнему использует старую реализацию, пока мы не закончим работу над этой функцией.

Например, мы использовали BBA, когда решили реализовать новый дизайн поиска на главной странице в QIWI кошельке.



Как мы используем флаги функций в мобильном приложении QIWI Кошелек

Помимо того, что флаги функций позволяют нам переключаться между старым и новым функционалом, они также предоставляют следующие возможности:

  • А/Б тесты.

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

    Это позволяет разделить пользователей на группы и предоставить им разные варианты использования той или иной функции.

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

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



Создание собственной библиотеки для флагов функций

Чтобы начать использовать флаги функций в нашем приложении, мы сначала решили посмотреть, какие решения с открытым исходным кодом были доступны на тот момент. По сути, это были различные плагины для Gradle, позволяющие на этапе сборки собрать приложение с некоторым набором флагов, попадающих, например, в класс BuildConfig в виде констант. Затем в коде мы можем соответственно обратиться к этим флагам и выбрать, какой вариант функции нам следует использовать.

Но этот подход имеет ряд недостатков:

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

    ,

  • Не существует нескольких источников, из которых можно получить и использовать флаги функций.

  • Не совсем удобно, когда в коде есть ветки, где ты в зависимости от флага выбираешь, какой функционал использовать.

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

Изначально библиотека состояла из одного модуля и располагалась непосредственно в репозитории приложения.

Для асинхронных операций библиотека использовала фреймворк RxJava 2. Мы его немного переписали, используя Kotlin Coroutines и разделив на несколько модулей.

Библиотека состоит из нескольких модулей:

  • компилятор — генерация кода на основе аннотаций;
  • менеджер функций — основной модуль библиотеки;
  • конвертер-gson, конвертер-джексон — различные варианты конвертеров на основе популярных библиотек для парсинга флагов функций из json;
  • источник данных-firebase, источник данных-agconnect — интеграция с Firebase Remote Config и Huawei Remote Config.
  • удаленный источник данных — пример реализации источника флагов на основе вашей собственной конечной точки http с помощью config.
  • отладка источника данных — позволяет менять флаги функций во время выполнения с помощью adb (для отладочных сборок).

Давайте посмотрим, как мы используем эту библиотеку.

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

FeatureManager имеет довольно простой контракт. Мы можем получить от него фичу, просто указав ее тип, а можем и указать свою собственную фабрику.

Это необходимо в том случае, если фабрике требуются некоторые зависимости для создания реализации функции.

  
  
  
  
  
  
  
  
  
  
  
   

interface FeatureManager { fun <Flag> getFeatureFlag(flagClass: Class<Flag>): Flag? fun <Feature> getFeature(featureClass: Class<Feature>): Feature fun <Feature, Flag> getFeature(featureClass: Class<Feature>, factory: FeatureFactory<Feature, Flag>): Feature }

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



@FeatureFlag("search_config") data class SearchConfigFlag( @JsonProperty("newSearch") val newSearch: Boolean, @JsonProperty("remoteSearch") val remoteSearch: Boolean)

Также для создания любой из реализаций используется FeatureFactory — абстрактный класс, от которого нужно наследовать.

Использование флага функции, который входит в метод createFeature , мы можем создать какую-то реализацию этой функции.



@Factory class SearchConfigFactory: FeatureFactory<SearchConfig, SearchConfigFlag>() { override fun createFeature(flag: SearchConfigFlag): SearchConfig = if(flag.newSearch) { NewSearchConfig(flag.remoteSearch) } else { createDefault() } override fun createDefault(): SearchConfig = OldSearchConfig() }

Чтобы библиотека знала, что такое флаги функций и фабрики для них, используются аннотации.

@FeatureFlag И @Фабрика .

С помощью процессора аннотаций генерируются реестры с флагами и фабриками.



@Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) annotation class FeatureFlag ( val key: String )



@Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) annotation class Factory

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



Особенности реализации библиотеки

Давайте посмотрим, как это работает под капотом.

Точкой входа в библиотеку является интерфейс FeatureManager. Реализация FeatureManager использует интерфейс FeatureCreator, который используется для создания функций.

Чтобы FeatureCreator знал, какие текущие флаги и фабрики он имеет в данный момент, существуют классы реестра для флагов (FeatureFlagRegistry) и фабрик (FeatureFactoryRegistry).

Это те самые классы, которые автоматически генерируются с помощью аннотаций.

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

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

Также есть исходники, которые реализованы на базе сервиса Firebase Remote Config, если собираем приложения для Google Play, или сервиса Huawei Remote Config, если скачиваем для AppGallery. Другой источник скачивает флаги с нашего сервера QIWI. Загруженные флаги из сетевых источников попадают в локальный кеш.

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



Как мы используем флаги функций в мобильном приложении QIWI Кошелек



FeatureCreator

FeatureCreator используется для создания объекта функции.

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

Обновить значения флагов функций можно в FeatureCreator.

interface FeatureCreator { fun <Feature> createFeature(featureClass: Class<Feature>): Feature fun <Feature, Flag> createFeature(featureClass: Class<Feature>, factory: FeatureFactory<Feature, Flag>): Feature }

Чтобы создать функцию, мы сначала находим для нее фабрику.

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

Он использует отражение и генерирует реестры для флагов и фабрик.

Если текущего флага нет, мы создаем функцию с конфигурацией по умолчанию.

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



class RealFeatureCreator( private val flagRegistry: FeatureFlagRegistry, private val factoryRegistry: FeatureFactoryRegistry, private val storage: ActualFlagsStorage, private val logger: Logger ) : FeatureCreator { @Suppress("UNCHECKED_CAST") override fun <Feature> createFeature(featureClass: Class<Feature>): Feature { //Find Pair of key and factory class in registry using provided feature class. val factoryWithKey = factoryRegistry.getFactoryMap()[featureClass] ?: throw getFeatureCreatorException(featureClass.simpleName) //Obtain feature key. val featureKey = factoryWithKey.first // Create instance of factory class using its first constructor. val factory = factoryWithKey.second.constructors.first().

newInstance() as FeatureFactory<Feature, Any> return createFeature(featureKey, factory) } @Suppress("UNCHECKED_CAST") override fun <Feature, Flag> createFeature(featureClass: Class<Feature>, factory: FeatureFactory<Feature, Flag>): Feature { //Because factory is already provided, we can use its generic type parameter to get feature flag class. val featureFlagClass = (factory::class.java.genericSuperclass as ParameterizedType).

actualTypeArguments[1] as Class<Feature> //Obtain feature key using reversed map in flag registry. val featureKey = flagRegistry.getFeatureKeysMap()[featureFlagClass] ?: throw getFeatureCreatorException(featureClass.simpleName) return createFeature(featureKey, factory) } @Suppress("UNCHECKED_CAST") private fun <Feature, Flag> createFeature(featureKey: String, factory: FeatureFactory<Feature, Flag>): Feature { //First we need to find actual feature flag object using key and cast it to Flag val featureFlag = storage.getFlag(featureKey) as Flag return if(featureFlag != null) { factory.createFeature(featureFlag) } else { //If feature flag is null we can ask to create feature using its default implementation. factory.createDefault().

also { logDefaultFlagWarning(featureKey) } } }



FeatureRepository

Реализация интерфейса FeatureRepository загружает и объединяет флаги из нескольких источников в зависимости от приоритета.

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



internal class RealFeatureRepository( private val dataSources: List<FeatureFlagDataSource>, private val cachedFlagsStorage: CachedFlagsStorage, private val flagRegistry: FeatureFlagRegistry, private val converter: FeatureFlagConverter, private val logger: Logger ): FeatureRepository { override fun getFlags(): Flow<FeatureFlagsContainer> { val allSources = dataSources.map { dataSource -> dataSource.getFlags(flagRegistry, converter, logger) .

map<Map<String, Any>, PrioritizedFlags?> { flags -> PrioritizedFlags( flags, dataSource.sourceType, dataSource.priority, dataSource.key ) }.

onStart { emit(null) }.

catch { emit( PrioritizedFlags( emptyMap(), dataSource.sourceType, dataSource.priority, dataSource.key ) ) } } return combine(allSources) { prioritizedFlags -> val presentFlags = prioritizedFlags .

filterNotNull() val actualFlags = presentFlags .

sortedBy { pFlags -> pFlags.priority } .

flatMap { pFlags -> pFlags.flags.map { it.key to it.value } } .

toMap() val sources = presentFlags.map { it.source }.

toSet() val keys = presentFlags.map { it.key }.

toSet() FeatureFlagsContainer(actualFlags, sources, keys) } } override fun getDataSourceKeys(): Set<String> = dataSources.map { it.key }.

toSet() override suspend fun saveFlags(flags: Map<String, Any>) { cachedFlagsStorage.saveFlags(flags, converter, logger) }



FeatureFlagDataSource

FeatureFlagDataSource — это интерфейс, имеющий несколько реализаций для разных источников.

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



interface FeatureFlagDataSource { /** * Loads feature flags from this DataSource. * DataSource can load and update feature flags config any time by emitting value into result [Flow].

* * @return A [Flow] of [Map] where key is feature key and value is object that represents feature flag. * * @param registry A [FeatureFlagRegistry] that can be used to map feature flag key to feature flag class. * @param converter A [FeatureFlagConverter] that can be used to convert feature flag from Json string into object. * @param logger A [Logger] that can be used to log any events that will occur while loading feature flags. */ fun getFlags( registry: FeatureFlagRegistry, converter: FeatureFlagConverter, logger: Logger ): Flow<Map<String, Any>> /** * Unique key for this [FeatureFlagDataSource].

*/ val key: String /** * Type of this [FeatureFlagDataSource].

* @see [FeatureFlagsSourceType].

*/ val sourceType: FeatureFlagsSourceType /** * Priority for feature flags from this [FeatureFlagDataSource].

* * If multiple [FeatureFlagDataSource] return flag with same key, * flag from [FeatureFlagDataSource] with biggest priority will be used. */ val priority: Int }

Для анализа флага функции из json в объект предоставляется интерфейс FeatureFlagConverter. Для реализации вы можете использовать любую библиотеку синтаксического анализа.

В нашем проекте используется Jackson, но есть и пример реализации на базе Gson. Чтобы проанализировать json с использованием сгенерированного FeatureFlagRegistry, мы находим необходимый класс и анализируем содержимое json с помощью конвертера.

Если не удалось разобрать, логируем это исключение.



fun FeatureFlagConverter.convertFeatureFlag( flagKey: String, flagValue: Any, sourceKey: String, registry: FeatureFlagRegistry, logger: Logger ): Pair<String, Any>? { val flagClass = registry.getFeatureFlagsMap()[flagKey] return if (flagClass != null) { try { Pair(flagKey, convert(flagValue, flagClass)) } catch (e: Throwable) { logger.logConverterException(sourceKey, flagKey, e) null } } else { null } }



FeatureFlagRegistry и FeatureFactoryRegistry

FeatureFlagRegistry позволяет получить карту, где ключом является имя флага функции, а значением — класс флага функции.

Также существует метод получения обратной карты.



interface FeatureFlagRegistry { /** * Returns map where key is feature key and value is feature flag class. */ fun getFeatureFlagsMap(): Map<String, Class<*>> /** * Returns map where key is feature flag class and value is feature key. */ fun getFeatureKeysMap(): Map<Class<*>, String> }

FeatureFactoryRegistry позволяет получить карту, где ключом является класс фичи-флага, а значением — класс фабрики для создания фичи.



interface FeatureFactoryRegistry { /** * Returns map where key is feature class and value is [Pair] of feature key and factory class. */ fun getFactoryMap(): Map<Class<*>, Pair<String, Class<*>>> }

Реализации этих интерфейсов генерируются из аннотаций с использованием процессора аннотаций и библиотеки.

КотлинПоэт .

Сейчас использую плагин капитан , но мы планируем мигрировать на Обработка символов Котлина после того, как ее начинает поддерживать другая библиотека генерации кода, Dagger.

Полученные результаты

Результаты, которых мы достигли благодаря созданию библиотеки:
  • Нам удалось реализовать TBD довольно быстро.

    Продуктовые команды смогли быстро добавить в проект фич-флаги и фичи и решить проблему, когда приложение нужно выпустить, но какой-то функционал не готов.

  • Мы начали проводить A/B-тесты, потому что это стало удобно.

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

  • Если мы вдруг обнаружим какой-то неприятный баг в новом функционале, мы сможем быстро его отключить, не выпуская исправление для приложения в маркете.

Часто бывает, что для решения какой-то задачи в OpenSource уже есть готовое решение, которое вас устроит. Но бывает, как и в нашем случае, что не существует библиотеки, которая бы полностью удовлетворяла всем нашим требованиям.

Мы решили рискнуть, написали свой и успешно используем его в производстве.

Так что не бойтесь экспериментировать и создавайте свои собственные решения для упрощения разработки.

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

Спасибо! Теги: #Разработка Android #Разработка мобильных приложений #программирование #Kotlin #переключение функций #флаги функций #разработка на основе транков

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.