tl;dr

Подход Feature toggle - про фичу, как абстракцию. Каждая может переключаться между состояниями: вкл/выкл. Основная сложность в локализации места включения - toggle point. Дополнительная сложность в желании разнести место ветвления и место принятия решения о включенности. В SOA архитектуре предлагается делать сервис для глобальных фич приложения, или выносить это в хэлпер VC/Presenter’а для локальных фич конкретного экрана.

Подготовил репку с примерами, смотреть тут.

Предыстория

Мне нравится англоязычный подкаст по iOS-разработке iPhreaks, и в один прекрасный момент я набрел на соседний рубевый подкаст про feature toggles. Все началось с легкого наброса про то, что не стоит ребейсить, продолжилось про интереснейший подход к ведению веток TBD и закончилось feature toggles.

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

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

В процессе подготовки читал разные источники, но ключевым стал этот пост в блоге Фаулера, там автор разбирает данный вопрос с примерами из js, я же попробовал адаптировать это к реалиям iOS-разработки, естественно, ничего теоретически нового я не придумал.

Забегая вперед, напишу, что позже с этой темой я выступил на локальной Рамблеровской конфе Rambler.iOS, а потом и на питерской Mobius. Надо сказать, что как раз на последней народ принял довольно прохладно, и в комментах было и про банально, и про то что не надо, а если надо, то делается просто. Отдельно были комменты про скомканный материал(мы с коллегой решили уместиться в один слот, чтобы народ не скучал, видимо получилось слишком бодро) и про то, что слишком много теории, нужна практика и примеры.

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

Теория

Концепция

В данном подходе есть несколько основных моментов:

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

Категории фич

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

Статические

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

Динамические

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

Примеры

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

Для простоты понимания и чтобы абстрагироваться от какой либо конкретной архитектуры, буду использовать MVC + FeatureService, для принятия решения о включенности динамических фич. Вот теперь точно поехали!

Статические

Пример 1 (Передача конфига)

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

Как будет выглядить ветвление логики внутри:

- (void)setupWithConfig:(ViewControllerConfig *)config {
    // Toggle point
    if (self.config.articleWasPaid) {
        [self setupPaidArticle];
    } else {
        [self setupWithUnpaidArticle];
    }
}

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

Пример 2 (Настройка зависимостей)

У нас есть табличка, где мы отображаем список постов, туда мы хотим подмешать рекламу. Контроллер сам ничего не грузит, а получает все данные у своих провайдеров:

@property (nonatomic, strong) ArticlesDataProvider *articlesDataProvider;
@property (nonatomic, strong) AdDataProvider *adDataProvider;

Мы просим данные у провайдера статей и у провайдера новостей, если второго нет, мы просто ничего не получим, все довольно просто:

NSArray *articles = [self.articlesDataProvider provideData];
NSArray *ads = [self.adDataProvider provideData];
self.resultArticles = [self mixAds:ads withArticles:articles];
[self.tableView reloadData];

Как и в первом случае место управления фичей находится за пределом модуля, да еще в случае с зависимостями не требуются ни проверки, ни места ветвления, все работает само.

Пример 3 (Сервис настроек)

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

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

FeatureServiceViewController.m:

- (void)viewDidLoad {
    BOOL hasToShowAd = [self.featureService hasToShowAd];
    if (hasToShowAd) {
        [self showAd];
    }
}

Контроллер обращается к специальному сервису за этими знаниями и действует в соответствии с ответом. Как выглядит сервис внутри:

FeatureServiceImplementation.m

- (BOOL)hasToShowAd {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    NSDate *adWasShownLastTime = [userDefaults valueForKey:kAdWasShownLastTimeIdentifier];
    adWasShownLastTime = adWasShownLastTime ?: [NSDate distantPast];
    NSDate *thresholdDate = [NSDate dateWithTimeIntervalSinceNow:-kAdShowThreshold];

    if ([adWasShownLastTime compare:thresholdDate] == NSOrderedAscending) {
        return YES;
    } else {
        return NO;
    }
}

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

Еще больше подробностей

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

Отсутствие логики принятия решения в VC

  • Мы не тянем в контроллер зависимости, которые ему не нужны не для чего кроме принятия решения о ветвлении
  • Если одна и та же фича используется в нескольких местах, то мы не копипастим код принятия решения

Место ветвления

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

NB!

Есть несколько моментов от которых хотелось бы предостеречь

Избыточная инкапсуляция

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

  • Это требует дополнительных усилий. Мы как хорошие программисты должны поставлять максимально хорошо написанный продукт в заданные сроки. За гибкость мы расплатимся дополнительным временем на разработку. В случаях с фичами, которые ну никогда не будут выключены, это будет пустой тратой времени
  • Гибкость сама по себе в данном случае может пойти во вред, потому что то, что раньше было частью каркаса нашего приложения и работало всегда, стало динамичным. Отсюда могут возникать проблемы с надстройкой других фич над данной

Зависимые фичи

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

Дерево зависимостей

  • Фича E зависит уже не только от своего состояния, но и от состояния фич B и A. Из-за этого усложняется управление включенностью данной фичи
  • Увеличивается сложность тестирования. В тестах на включенность фичи E необходимо дописать тесты на включенность фич A и B. При ручном тестировании понадобится включать все возможные комбинации фич, а количество комбинаций растет по экспоненте.
  • Увеличивается сложность как добавления фичи в дерево, так и удаления из него. Затрагивает множество тестов.

Что получаем?

  • Инкапсуляцию фич. Это само по себе здорово, потому что это дает нам власть над фичами, мы легко можем их включать/выключать в том числе и удаленно.
  • Простой переход от статических к динамическим фичам и обратно. Еще вчера мы включали фичу у 10% пользователей, сегодня мы включили ее для всех.
  • Спокойствие за опасные фичи. Иногда часть функционала приложения реализуется при помощи сторонних API, например, если ваша компания договорилась о совместной фиче. Если разработка на той стороне продвигается не очень, и есть опасность, что у них все сломается, хорошо иметь способ скрыть данный функционал от пользователя. Это легко реализуется при помощи конфига с сервера и подхода Feature toggle

Итоги

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

Напишите мне в комментариях или мне в twitter, что думаете. Буду признателен за фидбек.