Всем привет, меня зовут Илья, я андроид инженер. Почти три года назад, мы начали свой проект в сфере финтех[ссылка удалена модератором]. Срок запуска MVP был оптимистичным. За неделю до наступления дедлайна наша команда осознала, что срок запуска переносить никто не собирается, а одна фича вряд ли будет закончена вовремя. Рисковать не хотелось и было решено - прикрыть эту часть заглушкой. Блокируя часть нерабочей функциональности, мы питали надежды, что скоуп MVP будет закрыт. По-этому, выключить нерабочий код хотели так, чтобы по щелчку кнопки CI/Web экран стал доступен для пользователя.

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

Откровенно говоря, разговоры внутри команды о внедрении FeatureToggling шли уже давно. Эта история стала спусковым крючком для его внедрения.

Что это такое?

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

Применение:

  • Форсирование функциональности

    Основа feature toggle - уметь прикрыть любой участок кода по условию. Вы можете делиться своими наработками еще до того, как ваша фича будет полностью готова. Для разных сборок можем выставить различные флаги и получим, что для разработчиков доступны все фичи, для QA то, что можно тестировать и для пользователей, что можно трогать. Кроме этого, вы можете рассмотреть возможность перехода c GitFlow на TBD. Без форсирования функциональности, сделать это будет не так-то просто. 

    Пока мы не поднимем крышку, мы не будем знать что на сковородке
    Пока мы не поднимем крышку, мы не будем знать что на сковородке
  • A/Б тестирование

    Предположим, у вас есть гипотеза, что пользователи кулинарного приложения с ума сходят от окрошки на кефире. Но менеджер так не считает и хочет продвигать на первых страницах окрошку на квасе. Разрешить этот спор должен сам пользователь. Можно устроить опросник на странице открыв свою маркетинговую компанию. Но это будет выглядеть слишком навязчиво и отвлекать от основного контента. С помощью A/Б тестирования есть возможность произвести это скрытно для пользователя. Так, что тот и не догадается какой суп варится на кухне.

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

    Не обязательно иметь только 2 условия для проведения эксперимента. Гипотеза может содержать множество входных данных и исходов.

    Окрошка на кефире или на квасе? а побеждает пюрешка с котлеткой
    Окрошка на кефире или на квасе? а побеждает пюрешка с котлеткой

    Обычно, анализ данных выполняется на той стороне, которая ответственна за их выдачу. В нашем случае это будет backend, а разработчики клиентов красят кнопки. Здесь это только пример. Для клиентов будет более реалистичная картинка - как в UI это будет представлено: вертикальный список, горизонтальный скролл, табличный формат и различные вариации представления UI.

  • Удаленное управление

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

    Так же, есть возможности управление фиче флагами на лету. Однако, это довольно дорогое удовольствие и встречается в редком количестве продуктов. Сложно в архитектурной проработке и поддержке такой функциональности.

  • Тестирование

    Если можно прикрыть участок кода feature flag-ом, то можем отгородить часть функциональности от посторонних глаз. Одна и та же сборка будет содержать несколько вариации имплементации. Таким образом, при передаче билда в команду тестирования, пишем инструкцию по переключению флагов и даем возможность прогнать все необходимые варианты. Точно так же поступаем с продвижением по цепочке до релиза(внутреннее тестирование, альфа-бета тестирование, прод)


Как приготовить?

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

Фиче флаги можно переключать следующими способами:

  • хардкодом

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

    data class SoupFeatureToggle(
        val okroshkaType: OkroshkaType = OkroshkaType.KEFIR,
    ) {
        enum class OkroshkaType {
            KEFIR,
            KVASS,
    				;
        }
    		companion object {
    				val DEFAULT = SoupFeatureToggle()
    		}
    }
    //....
    fun showSoup() {
    		val featureToggle: SoupFeatureToggle = SoupFeatureToggle.DEFAULT
        when(featureToggle.okroshkaType) {
            SoupFeatureToggle.OkroshkaType.KEFIR -> showKefirOkroshka()
            SoupFeatureToggle.OkroshkaType.KVASS -> showKvassOrkoshka()
        }
    }

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

  • конфигурационно

    На различных флейворах мы хотим иметь уникальные настройки. Бегать с гвоздометом и переопределять для каждого типа сборок свой конфиг - можно, но это не элегантно. Представьте, что для каждой сборки(передача в QA, отправка в Internal/Alpha/Beta тестирование, продакшон) нужно будет выдернуть одного разработчика, который каждый раз будет лезть в код, перебивать и валидировать конфиг... Муторно и не интересно. Ведь при хорошем подходе, это можно отдать в руки самому менеджеру в формате JSON/XML/YAML… Да хоть Excel! Нам только нужно научиться парсить такой файл и принимать его во внимание. Если без Excel, права доступа можно раздать для ограничения списка лиц, которые могут быть причастны к изменениям.

  • через файловую систему

    На базе предыдущих наработок с конфигурационными файлами, нам достаточно добавить новый источник данных. Например, не вшитый в вашу сборку feature_toggle_config.json который располагается в ресурсах/ассетах, а локальный или удаленный файл. Как этот файл окажется в устройстве пользователя - не столь важная задача(CLI, файловый менеджер, удаленный репозиторий, 3rd party service).

  • в телефоне

    Такой тип переключения будет удобен для внутреннего тестирования. Вы написали инструкцию и документацию, отдали QA билд. Ранее описанные способы переключения флагов очень удобны на рабочем месте, но сложны в использовании, например, в метро. Пишем экран с переключателями состояний для каждого типа данных: string → EditText, boolean → checkbox/switcher, enum - popup/radiobutton и так далее. Не забываем про то, что этот экран не должен попасть до конечного пользователя, прячем его за dev/qa flavor и в бой.
    ———————————————————————————————————————————————
    Ранее описанные способы идеально ложатся на структуру, когда ваши флаги будут находится в актуальном состоянии на момент запуска процесса вашего приложения. Итоговая конфигурация собирается ДО момента инициализации IoC(DependencyIjection, ServiceLocator или что там у вас) фреймворка. В противном случае, нам придется решать задачу динамического разрула зависимостей на лету. Пример возможной ситуации:

    На картинке окрошка на квасе, а получили меню окрошки на кефире
    На картинке окрошка на квасе, а получили меню окрошки на кефире

    Заголовок построен на базе фиче флага который был получен ДО момента отрисовки. После отрисовки заголовка, к нам прилетели изменения фиче флагов. Рецепт какого блюда будет отрисован после нажатия кнопки - "Показать рецепт"?

    Получается, что жизненный цикл фиче флагов "на лету" нельзя будет определить на временной шкале. Они могут измениться в любой момент, что и рождает проблемы с их применением. Это отдельная история, которая должна решаться в зависимости от типа вашего проекта и требований. Не углубляюсь в эту тему, это про поставку зависимостей и их жизненный цикл. Просто подчеркиваю - 2 основных типа реализации изменений feature toggles: статический и на лету.

Как приготовить?

  1. FeatureToggleContainer

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

    Основная точка регистрации любого FeatureToggle. Все что не попадет в Registrar будет проигнорировано. Просто объявите контейнер и перечислите все фиче флаги. Это нужно для того, чтобы правильно сконфигурировать наш IoC framework и настроить защиту от неправильной конфигурации через тесты. В контейнер должен попадать именно инстанс объекта, а не декларация класса - необходимое условие для дешевого решения фолбэка. В случае отсутствия настроек в дальнейших слоях, будет принята настройка по умолчанию. По-этому, примите это во внимание, чтобы недоработанная функциональность не попала в рабочую зону. Как правило, декларации в контейнере должны быть такими, как будто все фиче тоглы выключены.

    private val featureToggleContainer: FeatureToggleContainer = SimpleFeatureToggleContainer(
            featureToggles = setOf(
                SampleTitleFeatureToggle(
                    enabled = false,
                    title = "Sample FeatureToggle",
                ),
                RestaurantInfoFeatureToggle(
                    mapVisible = false,
                    deliveryCostsVisible = false,
                    ratingVisible = false,
                ),
                MenuItemFeatureToggle(
                    enabled = false,
                    gridCount = 3,
                    type = MenuItemFeatureToggle.PreviewType.HORIZONTAL_LIST,
                    addToCartAvailable = true,
                )
            )
        )
    FeatureToggleContainerHolder.init(featureToggleContainer)

  2. Default config

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

    Конфигурация сборки, которую удобно поставлять через ресурсы. Может прятаться через любую директорию, которую можно разбить по flavors/buildType - raw, xml,assets и так далее. Либо же, создать свой compile time solution который будет вынимать их и перегружать значение.

    Забегая вперед, удаленный конфиг строится на базе Firebase. У него есть настройка конфигурации по умолчанию из файла ресурсов. Так как удаленный тип может быть недоступен для dev сборки, строим ResourcesFeatureToggleReader на структуре такого же файла, чтобы не переучиваться:

    <?xml version="1.0" encoding="utf-8"?>
    <defaultsMap>
        <entry>
            <key>sample_title</key>
            <value>{
                "enabled":true,
                "title":"Sample Resources config Title"
                }
            </value>
        </entry>
    </defaultsMap>

    Перегружаем тоглер с ключом sample_title.title и изменяем у него шапку.

  3. Remote config

    Самая питательная часть. Обжаренный фарш с луком, чесноком и томатным соусом выкладываем на лист.

    Удаленный тип поставки флагов, предназначенный для экстренного управления на уже раскатанных билдах. Подразумевается идея того, что к этому способу вы будете приходить только в экстренных ситуациях, когда билд раскатан и изменения требуют траты времени инженерной команды - задействуем этот подход. Либо же в проверке гипотез - А/Б тестирование.

    Готовить его можно следующими способами - 3rd party решения или свое. Из готовых - Firebase Remote Config. Самый популярный пример его использования - A/Б тестинг, который у этой библиотеки только удаленный(да, с подкапотным кэшем, но, только с ремутным порождением флагов) с фолбэком на ресурсы. Это не эталон, А/Б тестинг должен рождаться с пропиской доли раскатки ещё в Default Config. Но это уже детали и с этим можно начинать.

    FirebaseFeatureToggleReader(
        fetchTimeout = 60.seconds,
        minimumFetchInterval = 12.hours,
        json = json,
        defaultConfigRes = defaultXmlConfigResource,
    )

    Задаем настройки для FirebaseFeatureToggleReader - таймаут и минимальный интервал для скачивания нового файла, десериализатор и конфиг по умолчанию(см. пункт 2). И дальше играемся с Web интерфейсом и пробуем наши настройки в бою.

  4. Debug file config

    Все посыпаем тертым сыром пармезан.

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

    XmlFileFeatureToggleReader(
        json = json,
        xmlConfigReader = XmlFileConfigReader(),
        xmlConfigFileProvider = DefaultConfigFileProvider(
            applicationContext = context,
        ),
    )

    Необходимо это потому, что в ваших конфигурациях могут быть спрятаны уникальные/платные/неготовые фичи, ответственность за нерушимость которых у вас прописана в terms of conditions. Теперь, для форсирования функциональности необходимо отредактировать файл на самом гаджете, либо же перезалить новый с помощью adb. Не нужно пересобирать билд, переключать флаги на RemoteConfig и ждать пока выкачается новый скрипт. Делаем все максимально быстро и доступно.

  5. Debug panel

    Обильно поливаем сыр соусом бешамель.

    Файл это конечно хорошо. Но каждый раз ползать в утилиты на телефоне для редактирования конфига либо иметь нотбук под рукой для загрузки - нудобно. Да и мы ж ленивые инженеры. По-этому и дописываем отдельный лаунчер/экран/меню, который даст возможность его модификации на телефоне с GUI и без SMS.

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

    devImplementation("io.github.ilyapavlovskii:debug.panel:X.X.X")

    Для запуска такой дебаг панели, вам всего-лишь необходимо добавить зависимость к проекту и в вашем flavor добавиться еще один launcher. Просто убедитесь в том, что FeatureToggleReaderHolder и FeatureToggleContainerHolder были заданы, а остальное взведется автоматом.

Bon appetit

На выходе получаем картинку с конфигураторами по приоритету: дебажный файлик превыше всего, удаленный репо, локальный конфиг и дефолтная конфигурация. Оставляем возможность для вставки произвольных FeatureToggleReader в нашу цепочку и получаем на выходе ChainOfResponsibility считывателей конфигураций.

val featureToggleReader = ChainFeatureToggleReader(
    featureReaders = arrayOf(
                      // Debug file config reader
        XmlFileFeatureToggleReader(
            json = json,
            xmlConfigReader = XmlFileConfigReader(),
            xmlConfigFileProvider = DefaultConfigFileProvider(
                applicationContext = context,
            ),
        ),
                      // Remote feature toggle reader
        FirebaseFeatureToggleReader(
            fetchTimeout = 60.toDuration(DurationUnit.SECONDS),
            minimumFetchInterval = 12.toDuration(DurationUnit.HOURS),
            json = json,
            defaultConfigRes = defaultXmlConfigResource,
        ),
                      // Default config reader
        ResourcesFeatureToggleReader(
            json = json,
            configReader = xmlConfigReader,
        ),
    )
)
FeatureToggleReaderHolder.init(featureToggleReader)

И все это безобразие со схемой подключения и примером хранится здесь.

Комментарии (6)


  1. YegorP
    09.01.2023 04:16
    +2

    Мы с тогглами не велосипедили, а просто взяли Unleash. Полёт нормальный, но...

    Применительно к приложениям (не обязательно мобильным) вы с этими тогглами получаете головняк в плане поддержки "старых" версий. Типа: начали пилить фичу; спрятали её под тоггл; выкатили версию приложения 1 с этим тогглом; постепенно ввели фичу в эксплуатацию (открыли краник на 100% в том же анлише) — и всё, версия 1 теперь обречена зависеть от тоггл-менеджмента, причём от конкретной его конфигурации. Фича-то допилена, обкатана, можно вырезать тоггл над ней в версии 2, но в версии 1 на устройствах пользователей этот тоггл уже никуда не денется — вам придётся поддерживать инфраструктуру тоггл-менеджмента.

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


    1. baklajan
      09.01.2023 04:32
      +1

      Думаю для этого можно ввести понятие минимальной версии клиентского приложения, и сделать модалку, если версия у клиента ниже минимальной версии. Мы реализовали это так (примерно) - клиент отправляет запрос в API, получает MINIMAL и LAST_VERSION. Если версия ниже MINIMAL, то блочит все приложение модалкой, предлагая обновиться. Если версия между MINIMAL и LAST_VERSION, то просто выходит модалка (которую можно закрыть), информирующая клиента о том, что вышла новая версия приложения.


      1. TranE91 Автор
        09.01.2023 04:38

        Есть нативное решение, но мне оно очень не нравится, но это уже тема отдельного топика.

        https://developer.android.com/guide/playcore/in-app-updates


        1. baklajan
          09.01.2023 05:44
          +1

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


  1. vikarti
    09.01.2023 07:41
    +1

    Что у на том проекте где я сейчас работаю:


    • есть именно тоглы. у тогла есть состояние вкл/выкл, если вкл для всех — то еще и версия где он вкл.
    • раз включенный для всех тогл — нельзя выключать по правилам но при этом через несколько релизов надо удалить
    • на сборках для разработчиков и тестеров есть панелька для включения тоглов руками (работает в том числе и на прод-серверах)
    • есть еще динамические настройки, с сервера приходя флаги про режим работы функционала (и есть вариант — "вырубить нафиг", если что-то пошло не так и фича начала создавать проблему на фронте или бэке, иногда выключенная фича означает "использовать старую версию того же" но чаще именно полное отключение фичи пока чинят).
    • по правилам, фича закрывается тоглом И динамической настройкой. И опять же постепенно выпиливается когда уже не надо
    • динамические настройки учитывают еще и платформу(iOS/Android/Web) и версию приложения
    • на старте приложение может словить ответ от бэка что рекомендуется обновится либо обновление должно быть обязательным (зависит от версии приложения, устройства и так далее — вполне может быть что телефон с Android 6 потребуют обновится а вот с Android 4 — дадут пока пожить… потому что новые версии требуют минимум Android 6)

    И все это — самописное.


  1. madcomrad
    10.01.2023 03:37
    +1

    Спасибо автору за вкусную статью!