Override
目录
背景
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.enabled
。FeatureGroup
类支持任意深度嵌套,因此您可以在 FeatureGroup
中构建多个层级的 FeatureGroup
。
控制功能标志
继续我们的例子,您可能想了解如何控制您特点的 "enabled"
属性。但是,您不能直接这样做!这是由于 "enabled"
是布尔值,意味着它只有两种状态(true 和 false)。另一方面,功能标志有 四种不同的状态:开启、关闭、覆盖开启和覆盖关闭。
对于您的应用代码来说,大部分时间您只需关注两种基本状态 on
和 off
(这也是为什么 "enabled"
是布尔值)。这样可以使您的 if-语句更简单。然而,当您手动开启或关闭功能标志时,需要跟踪原始状态以便以后恢复。这将在我们讨论动态功能时变得非常重要。
让我们看看如何在运行时手动开启 blueText
。
@objc func toggleBlueTextButtonTapped() {
// Turn blueText ON
myFeatures.blueText.override = .enabled
}
执行上述代码后,功能 blueText
即被开启。也就是说,在下一个 example 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)
}
需要重启的功能
如果我们的所有功能标志都能立即生效那就太好了。在现实中,许多功能非常基本,无法在不重启应用的情况下开启或关闭。虽然覆盖永远不会代表重启您的应用,但它确实提供了一种模拟此要求的工具。让我们添加一个需要重启的功能...
@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
Override提供了一个简单的表格视图控制器FeaturesViewController
,它提供了一个通用的用户界面来管理特性标志。视图控制器显示来自给定FeatureRegistry
的可用特性列表。它还允许您使用文本搜索查找特性。每个特性的状态都被直观地描绘出来,并且安装了允许方便地进行特性控制的滑动手势。
特性状态是直观的
- 被覆盖的启用特性为绿色
- 被覆盖的禁用特性以红色显示
- 带有下划线的特性(绿色或红色)正在覆盖默认设置
通过手势控制特性状态
- 向左滑动以显示“开启”和“关闭”强制覆盖
- 向右滑动以恢复特性的默认状态
支持“需要重启”的特性
有时在应用加载完成后启用或禁用功能是不安全或不切实际的。为了支持这些情况,FeaturesViewController
意识到其显示的所有功能中的 restartRequired
参数。
在关闭 FeaturesViewController
之前,调用应用有责任处理任何重启要求。这里有两种方式可以实现
- 自动处理:在关闭
FeaturesViewController
之前调用presentRestartAlert(from:completion:)
方法,然后在完成处理程序中实际关闭视图控制器。 - 手动处理:检查
featuresRequiringRestart
属性的值,如果列表不为空,则手动触发重启。
测试支持
任何功能控制系统都必须提供丰富的单元测试支持。开发者通常希望在开启和关闭状态下测试功能。通过 FeatureTestSupport
类,Override 提供了这种支持。
测试配置
测试支持由单独的 CocoaPod 框架提供。要包含 Override 测试支持,请将 pod 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
}
}
}
此外,可以使用 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文件 了解如何参与其中。我们欢迎问题、疑问和拉取请求。拉取请求也欢迎。
维护者
- Adam Kaplan,Twitter: @adkap
- David Grandinetti,Twitter: @dbgrandi
许可证
本项目的许可协议为MIT开源许可证。完整条款请参阅LICENSE。