学习大佬的09|开关组件:如何使用功能开关,支持产品快速迭代有感。
开发过程中会遇到测试环境和线上环境区分的情况,比如,请求API的不同,又或者第三方SDK的APPKey的不同等等。大部分情况下开发过程中是直接使用下面类似的代码,用于环境判断。
#ifdef DEBUG
#define JPush_APPKey @"JPush_APPKey_DEBUG"
#else
#define JPush_APPKey @"JPush_APPKey_Release"
#endif
同样,也会有本地根据某个值来判断是否显示过什么东西的逻辑,比如引导页或者提示弹窗等等。
大部分情况下开发的时候,是用UserDefaults存储一个bool值,用于判断。比如:
// 存储
UserDefaults.standard.setValue(value, forKey: keyStr)
UserDefaults.standard.synchronize()
// 判断
if let value = UserDefaults.standard.value(forKey: keyStr) as? Bool {
...
}
最后还会有一种情况,就是服务端控制显示或不显示某个内容,比如A/B测试流量的开放等等。
这种情况下,做法是启动的时候调用服务端的接口,在返回的接口数据中存储对应的值,使用时直接获取存储的值。比如:
// 启动时
ApiManager().request(apiName, parameter: parameters, callBack: { (data) in
// 存储服务端返回的值
})
// 使用时
// 获取存储的值,用于判断
而在09|开关组件:如何使用功能开关,支持产品快速迭代中,作者把开关组件分为了三类:编译时开关、本地开关和远程开关
// TogglesDataStoreType.swift
protocol ToggleType { }
protocol TogglesDataStoreType {
func isToggleOn(_ toggle: ToggleType) -> Bool
func update(toggle: ToggleType, value: Bool)
}
定义一个枚举BuildTargetToggle继承自ToggleType,定义BuildTargetTogglesDataStore继承自TogglesDatatStoreType,并对外暴露单例对象,内部init设置对应的值,同时实现isToggleOn:方法,update:方法里面为空,因为是编译时开关,所以更新方法并不需要,但是要写
// BuildTargetTogglesDataStore.swift
enum BuildTargetToggle: ToggleType {
// 此处可以注意使用系统关键字之后的写法
case debug, `internal`, production
}
struct BuildTargetTogglesDataStore: TogglesDataStoreType {
static let shared: BuildTargetTogglesDataStore = .init()
private let buildTarget: BuildTargetToggle
private init() {
#if DEBUG
buildTarget = .debug
#endif
#if INTERNAL
buildTarget = .internal
#endif
#if PRODUCTION
buildTarget = .production
#endif
}
func isToggleOn(_ toggle: ToggleType) -> Bool {
guard let toggle = toggle as? BuildTargetToggle else {
return false
}
return toggle == buildTarget
}
func update(toggle: ToggleType, value: Bool) { }
}
定义一个一个枚举,InternalToggle继承自String和ToggleType,定义String是为了作为key存储,每增加一种类型,只需要在enum中添加case即可。定义InternalTogglesDataStore继承自TogglesDatatStoreType,对外暴露单例对象,内部init赋默认值,同时实现isToggleOn:和update:方法
// InternalTogglesDataStore.swift
enum InternalToggle: String, ToggleType {
case isLikeButtonForMomentEnabled
}
struct InternalTogglesDataStore: TogglesDataStoreType {
private let userDefaults: UserDefaults
private init(userDefaults: UserDefaults) {
self.userDefaults = userDefaults
// 可以注意userDefaults.register方法的使用,赋默认值
self.userDefaults.register(defaults: [
InternalToggle.isLikeButtonForMomentEnabled.rawValue: false
])
}
static let shared: InternalTogglesDataStore = .init(userDefaults: .standard)
func isToggleOn(_ toggle: ToggleType) -> Bool {
guard let toggle = toggle as? InternalToggle else {
return false
}
return userDefaults.bool(forKey: toggle.rawValue)
}
func update(toggle: ToggleType, value: Bool) {
guard let toggle = toggle as? InternalToggle else {
return
}
userDefaults.set(value, forKey: toggle.rawValue)
}
}
定义一个枚举,RemoteToggle继承自String和ToggleType,定义String也是为了使用key,每增加一种类型,只需要在enum添加case。定义RemoteTogglesDataStore继承自TogglesDataStoreType,对外暴露单例,内部init方法需要已初始化好的请求,同时实现isToggleOn:和update:方法,这个地方update的方法内容如果需要缓存则添加存储逻辑,如果不需要,则无需实现。
// RemoteTogglesDataStore.swift
enum RemoteToggle: String, ToggleType {
case isRoundedAvatar
}
struct RemoteTogglesDataStore: TogglesDataStoreType {
static let shared: RemoteTogglesDataStore = .init()
private let remoteConfigRepo: RemoteConfigRepoType
private init(remoteConfigRepo: RemoteConfigRepoType = RemoteConfigRepo.shared) {
self.remoteConfigRepo = remoteConfigRepo
}
func isToggleOn(_ toggle: ToggleType) -> Bool {
guard let toggle = toggle as? RemoteToggle else {
return false
}
guard let remoteConfigKey = FirebaseRemoteConfigKey(rawValue: toggle.rawValue) else {
return false
}
return remoteConfigRepo.getBool(by: remoteConfigKey)
}
func update(toggle: ToggleType, value: Bool) { }
}
再来看作者大佬总结的图示的实现原理:
BuildTargetToggle、InternalToggle、RemoteToggle都继承自ToggleType协议
BuildTargetToggleDatatStore、InternalTogglesDataStore、RemoteTogglesDataStore都继承自TogglesDataStoreType协议,且实现协议的两个方法
最后,用本地开关为例子,看一下使用:
let togglesDataStore: TogglesDataStoreType = BuildTargetTogglesDataStore.shared
if togglesDataStore.isToggleOn(BuildTargetToggle.debug) {
// debug模式
}
使用作者这种方式,把所有开关组件的处理都抽象成统一的协议,便于管理,使用上也更加方便。对比未抽象前的操作,会发现,未抽象前的就像打游击战,遍地开花,想要找到一个开关组件的地方,需要对业务代码熟悉,才能通过搜索找到对应地方。而抽象统一处理后,只需要知道对应的协议是什么,按照协议搜索有哪些实现,即可,方便快捷。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。