前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >开关组件对比

开关组件对比

原创
作者头像
莫空9081
修改2021-04-07 14:40:17
9130
修改2021-04-07 14:40:17
举报
文章被收录于专栏:iOS 备忘录iOS 备忘录

学习大佬的09|开关组件:如何使用功能开关,支持产品快速迭代有感。

背景

开发过程中会遇到测试环境和线上环境区分的情况,比如,请求API的不同,又或者第三方SDK的APPKey的不同等等。大部分情况下开发过程中是直接使用下面类似的代码,用于环境判断。

代码语言:javascript
复制
#ifdef DEBUG
    #define JPush_APPKey @"JPush_APPKey_DEBUG"
#else
    #define JPush_APPKey @"JPush_APPKey_Release"
#endif

同样,也会有本地根据某个值来判断是否显示过什么东西的逻辑,比如引导页或者提示弹窗等等。

大部分情况下开发的时候,是用UserDefaults存储一个bool值,用于判断。比如:

代码语言:javascript
复制
// 存储
UserDefaults.standard.setValue(value, forKey: keyStr)
UserDefaults.standard.synchronize()

// 判断
if let value = UserDefaults.standard.value(forKey: keyStr) as? Bool {
    ...
}

最后还会有一种情况,就是服务端控制显示或不显示某个内容,比如A/B测试流量的开放等等。

这种情况下,做法是启动的时候调用服务端的接口,在返回的接口数据中存储对应的值,使用时直接获取存储的值。比如:

代码语言:javascript
复制
// 启动时
ApiManager().request(apiName, parameter: parameters, callBack: { (data) in
    // 存储服务端返回的值
})

// 使用时
// 获取存储的值,用于判断

对比

而在09|开关组件:如何使用功能开关,支持产品快速迭代中,作者把开关组件分为了三类:编译时开关、本地开关和远程开关

  • 编译时开关:让编译器通过检查编译条件来启动后者关闭一些功能。
  • 本地开关:让用户在App里面手动启动或者关闭一些功能。
  • 远程开关:让产品经理远程遥控App来启动或者关闭一些功能。
  1. 通过Swift protocol定义了ToggleType和TogglesDataStoreType两个协议,并定义两个方法,判断开关是否打开、以及更新开关状态
代码语言:javascript
复制
// TogglesDataStoreType.swift

protocol ToggleType { }

protocol TogglesDataStoreType {
    func isToggleOn(_ toggle: ToggleType) -> Bool
    func update(toggle: ToggleType, value: Bool)
}
  1. 编译时开关实现:

定义一个枚举BuildTargetToggle继承自ToggleType,定义BuildTargetTogglesDataStore继承自TogglesDatatStoreType,并对外暴露单例对象,内部init设置对应的值,同时实现isToggleOn:方法,update:方法里面为空,因为是编译时开关,所以更新方法并不需要,但是要写

代码语言:javascript
复制
// 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) { }
}
  1. 本地开关实现:

定义一个一个枚举,InternalToggle继承自String和ToggleType,定义String是为了作为key存储,每增加一种类型,只需要在enum中添加case即可。定义InternalTogglesDataStore继承自TogglesDatatStoreType,对外暴露单例对象,内部init赋默认值,同时实现isToggleOn:和update:方法

代码语言:javascript
复制
// 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)
    }
}
  1. 远程开关的实现:

定义一个枚举,RemoteToggle继承自String和ToggleType,定义String也是为了使用key,每增加一种类型,只需要在enum添加case。定义RemoteTogglesDataStore继承自TogglesDataStoreType,对外暴露单例,内部init方法需要已初始化好的请求,同时实现isToggleOn:和update:方法,这个地方update的方法内容如果需要缓存则添加存储逻辑,如果不需要,则无需实现。

代码语言:javascript
复制
// 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协议,且实现协议的两个方法

最后,用本地开关为例子,看一下使用:

代码语言:javascript
复制
    let togglesDataStore: TogglesDataStoreType = BuildTargetTogglesDataStore.shared
    if togglesDataStore.isToggleOn(BuildTargetToggle.debug) {
        // debug模式
    }

总结

使用作者这种方式,把所有开关组件的处理都抽象成统一的协议,便于管理,使用上也更加方便。对比未抽象前的操作,会发现,未抽象前的就像打游击战,遍地开花,想要找到一个开关组件的地方,需要对业务代码熟悉,才能通过搜索找到对应地方。而抽象统一处理后,只需要知道对应的协议是什么,按照协议搜索有哪些实现,即可,方便快捷。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 对比
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档