YMOverride 2.5.0

YMOverride 2.5.0

Adam KaplanDavid GrandinettiDavid Grandinetti 维护。



  • Adam Kaplan 和 David Grandinetti

Override

CI Status

目录

背景

Override 是一个超容易使用的功能开关管理系统,适用于 iOS、tvOS、watchOS 和 macOS。Override 帮助最小化添加和维护大量功能开关所需的样板代码。它提供将功能逻辑上分组到可嵌套的功能组中的能力,并支持搜索特定标志。通常,应用开发者使用功能开关来管理对仍在开发中、实验性或处于 A/B 测试后的功能访问。具有简化的功能开关管理流程有助于通过消除新实验的障碍来推动创新。

功能开关通常有三种状态:开启、关闭或默认。某个功能的默认状态可能是一个预置模式或由远程配置或 A/B 测试系统定义。Override 支持这些用例,以及更多的用例!

安装

CocoaPods

CocoaPods 是 Cocoa 项目的依赖管理器。有关使用和安装说明,请访问他们的网站。要使用 CocoaPods 将 Override 整合到您的 Xcode 项目中,在您的 Podfile 中指定它

pod 'YMOverride', '~> 2.2'

SwiftPM

.package(url: "https://github.com/yahoo/Override.git", from: "2.5.0") 添加到您的 package.swift

使用方法

要使用 Override,您需要一个 FeatureRegistry 的子类。 Override 将检查您的注册类中所有类型的 AnyFeature 的实例属性。

基本功能

让我们创建一个基本功能 "blueText",当启用时将所有文本变为蓝色。为此,将一个名为 "blueText" 的属性添加到您的功能注册表中

import YMOverride

@objc class Features : FeatureRegistry {

    @objc let blueText = Feature()
}

这个简单的类提供了大量内容,但我们会稍后详细介绍。让我们看看如何使用此功能检测 "blueText" 功能是否启用。以下是一段您可以在您的应用中想象的代码

class ViewController : UIViewController {

    let myFeatures = Features()

    let label = UILabel()

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        self.view.addSubview(label)
        label.frame = view.frame
        label.text = "Hello World!"

        // Update text color based on feature flag
        label.textColor = myFeatures.blueText.enabled ? .blue : .green
    }
}

分组相关特性

对于具有大量特性标志的系统,Override通过FeatureGroup类提供了逻辑分组的手段。由FeatureGroup不会提供额外的运行时功能;它主要是用来组织特性标志。特性分组非常简单

import YMOverride

@objc class ListFeatures : FeatureGroup {
    @objc let infinteScroll = Feature()
}

@objc class Features : FeatureRegistry {

    @objc let list = ListFeatures()

    @objc let blueText = Feature()
}

现在,在代码中将infiniteScroll特性标志引用为myFeatures.list.infiniteScroll.enabledFeatureGroup类支持任意深度的嵌套,您可以在FeatureGroup内部构建多层的FeatureGroup

控制特性标志

继续我们的示例,下一个你可能想知道的问题是如何控制特性的.enabled属性。嗯,你不能直接这样做!这是因为.enabled是布尔值,意味着它只有两种状态(true和false)。另一方面,特性标志有四个不同的状态:开启、关闭、覆盖开启和覆盖关闭。

大多数情况下,你的应用代码只关心两个基本状态onoff(这也是为什么.enabled是布尔值的原因)。这可以让你的if语句更加简单。然而,当你手动启用或禁用特性标志时,需要跟踪以便在以后恢复原始状态。这将在我们讨论动态特性时变得非常重要。

让我们看看如何在运行时手动开启blueText

    @objc func toggleBlueTextButtonTapped() {
        // Turn blueText ON
        myFeatures.blueText.override = .enabled
    }

上面的代码执行后,特性blueText被启用。也就是说,文本将在接下来的viewWillAppear执行时变为蓝色:label.textColor = myFeatures.blueText ? .blue : .green。真酷!

同样,如果您希望无论其默认状态如何都关闭blueText,可以将值改为.disabled,如下所示

    @objc func toggleBlueTextButtonTapped() {
        // Turn blueText ON
        myFeatures.blueText.override = .disabled
    }

如果您想要移除任何自定义并将特性标志返回到其默认状态

    @objc func toggleBlueTextButtonTapped() {
        // Turn blueText OFF
        myFeatures.blueText.override = .featureDefault
    }

默认特性状态

好的,到目前为止,我们知道如何测试特性标志的值,以及如何覆盖特性标志的状态。接下来,我们需要了解如何定义特性标志的初始状态。默认情况下,特性标志是关闭的,在需要时可以打开。

让我们创建一个默认关闭,仅在调试时开启的特性。

@objc class Features : FeatureRegistry {

    @objc let blueText = Feature()

    @obj let debugLogging = Feature(defaultState: false)
}

需要重启的功能

如果所有功能标志都能立即生效,那就太完美了。但实际上,许多功能非常基础,无法在无需重启应用的情况下启用或禁用。虽然Override永远不会代表您重启应用,但它确实提供了一个方法来模拟这种需求。让我们添加一个需要重启的功能...

@objc class Features : FeatureRegistry {

    @objc let blueText = Feature()

    @obj let debugLogging = Feature(defaultState: false)

    @objc let useTabBarNav = Feature(requiresRestart: true) 
}

现在,当您更改useTabBarNav时,可以通过检查布尔属性useTabBarNav.requiresRestart来判断是否需要重启,并在您的应用代码中相应地处理。

[高级] 派生和动态功能

有时,我们需要一个功能,它的默认开/关状态取决于运行时环境的一些方面。比如说,你希望默认开启这个功能,但只在周二!我们可以通过使用DynamicFeature类型来实现这一点。

@objc class Features : FeatureRegistry {

    @objc let blueText = Feature()

    @obj let debugLogging = Feature(defaultState: false)

    @objc let useTabBarNav = Feature(requiresRestart: true)

    @objc let tuesdayExperiment = DynamicFeature() { _ in
        let components = Calendar.current.dateComponents(Set([.weekday]), from: Date())
        return components.weekday == 3
    }
}

DynamicFeature的提供代码块在需要默认状态时每次都会被评估,因此如果需要,值可以更改,或者您可以为性能缓存它!

[高级] 远程控制功能

到目前为止,我们讨论了仅存在于单个应用上下文中的本地功能。许多应用程序使用远程控制功能标志服务和实验平台。幸运的是,Override可以通过使用DynamicFeatures轻松支持这一点。

作为练习,让我们看看如何为Flurry的FConfig远程配置功能创建一个Override包装器(源代码

@objc class FConfigFeature : DynamicFeature {

    init(key: String? = nil, requiresRestart: Bool = false, configDefault: Bool = false) {

        // Delegate to FConfig, using the provided default parameter as FConfig default.
        super.init(key: key, requiresRestart: requiresRestart) { (feature: AnyFeature) -> Bool in
            return FConfig.sharedInstance.getBool(forKey: feature.key, withDefault: configDefault)
        }
    }
}

现在使用我们的功能注册表中的FConfigFeature只是更常见的做法

@objc class Features : FeatureRegistry {

    @objc let blueText = Feature()

    @obj let debugLogging = Feature(defaultState: false)

    @objc let useTabBarNav = Feature(requiresRestart: true) 

    @objc let tuesdayExperiment = DynamicFeature() { _ in
        let components = Calendar.current.dateComponents(Set([.weekday]), from: Date())
        return components.weekday == 3
    }

    // FConfig backed feature
    @objc let redButtons = FConfigFeature()
}

功能管理UI

使用简单的表格视图控制器FeaturesViewController重写船只,该控制器提供通用的用户界面用于管理功能标志。视图控制器显示了从给定的FeatureRegistry中可用的功能列表。它还允许您通过文本搜索来查找功能。每个功能状态都以图形方式表示,并安装了滑动手势,以便方便地控制功能。

功能状态以图形方式传达

  • 重写的启用功能呈绿色
  • 重写的禁用功能呈红色显示
  • 加下划线的功能(可能是绿色或红色)正在重写默认设置

功能状态可通过手势控制

  • 向左滑动以显示“开启”和“关闭”强制重写
  • 向右滑动以恢复功能的默认状态

支持“需要重启”的功能

有时在应用程序加载完成后启用或禁用功能是不安全的或不切实际的。为了支持这些情况,FeaturesViewController已了解所有显示功能中的restartRequired参数。

在关闭FeaturesViewController之前,调用应用程序必须处理任何重启要求。有两种方式可以这样做

  1. 自动处理:在关闭FeaturesViewController之前调用presentRestartAlert(from:completion:)方法,然后在完成处理程序中实际关闭视图控制器。
  2. 手动处理:检查featuresRequiringRestart属性的值,如果列表不为空,则手动触发重启。

测试支持

任何功能控制系统都需要丰富的单元测试支持。开发者通常想要在开启和关闭状态下测试功能。Override 通过FeatureTestSupport类来实现这一点。

测试设置

测试支持是在一个单独的CocoaPod框架中提供的。要包含Override测试支持,将pods OverrideTestSupport 添加到您的测试目标,如下所示

OverridePodVersion = '1.0.0'

target 'MyApp' do
    pod 'Override', OverridePodVersion

    target 'MyAppTests' do
        pod 'OverrideTestSupport', OverridePodVersion
    end
end

使用单元测试助手

Override 简化了测试功能矩阵的任务。您不必模拟或手动覆盖功能,而是可以使用 Override 提供的工具选择性地启用或禁用特定测试中的功能。

describe("sans serif font experiment") {
    it("respects the enabled flag") {
        withFeature(features.useSansSerifFont).enabled {
            // test or snapshot test for enabled state
        }
    }

    it("respects the enabled flag") {
        withFeature(features.useSansSerifFont).disabled {
            // test or snapshot test for disabled state
        }
    }
}

此外,Override 还可以轻松地通过使用 withFeatures() 方法一次性启用或禁用多个功能,如下所示

it("works with all experiments disabled") {
    withFeatures([features.useSansSerifFont, features.betaOnboarding]).enabled {
        // test with all listed features enabled
    }
}

该测试支持与 Objective-C 一起工作,使用类似的语法,如下所示

// (Just adds @ on the array literal, and `^()` after .enabled)
it(@"works with all experiments disabled"), {
    withFeature(features.useSansSerifFont).enabled(^{
        // ...
    });
});

运行示例

要运行示例项目,请克隆仓库,然后从 Example-Swift 和 Example-ObjC 目录中首先运行 pod install

贡献

有关如何参与的信息,请参阅 contributing.md 文件。我们欢迎问题、疑问和拉取请求。非常欢迎拉取请求。

维护者

许可证

本项目的许可证是 MIT 开源许可证。有关完整条款,请参阅 LICENSE