本文正在参与「金石方案 . 分割6万现金大奖」
前言
本篇技术含量不高,更多的偏重从事务层面考虑Protocol的封装。
接上回Swift:巧用module.modulemap,告别Bridging-Header.h,当我把友盟SDK集成好了之后,就准备开始开始着手点击工作的计算了。
直接调用友盟SDK的API就好了:
open class MobClick : NSObject {
open class func event(_ eventId: String!, attributes: [AnyHashable : Any]! = [:])
}
这个简略,作为一个API调用工程师,走起:
MobClick.event("redButtonClick", attributes: ["name": "seasonZhu", "webSite": "www.juejin.com"])
嗯,挺简略的,挺好的,能够散了。
撇开事务,仅仅调用这个接口,我想初学者都会,可是结合事务考虑,让埋点工作愈加简略才是咱们需求持续考虑的。
结合事务考虑
咱们先看看这个MobClick
这个event办法,需求传入的是String类型与字典,显然就这么持续硬编码,并不合符咱们的风格,一旦写错,排查起来十分费事,并且后面App的事务扩展,持续计算点击工作的成本也会十分高。
咱们一个一个参数的说,我先从这个eventId
开始说。
eventId入参的封装
避免硬编码最简略的办法便是界说一个常量,后续要运用的时分,调用这个常量即可。
let oneButtonClick = "oneButtonClick"
let twoButtonClick = "twoButtonClick"
.
.
.
就像上面这样的代码,比方我50个点击工作,就这么写50个就能够,这样做当然没有问题。
可是其实有的时分,咱们需求区分事务的点击工作的,这么写一大堆并不好,咱们需求一个前缀区分不同的页面。
你或许回想这有何难:
let AControllerOneButtonClick = "AControllerOneButtonClick"
let BControllerTwoButtonClick = "BControllerTwoButtonClick"
.
.
.
嗯这样写当然没有问题,可是看起来十分费力,也不易于保护。
很多Swift的代码已经给出了示范,咱们做简略的处理就能够了:
enum AController {
static let oneButtonClick = "oneButtonClick"
.
.
.
}
enum BController {
static let twoButtonClick = "twoButtonClick"
.
.
.
}
这样的话,我调用的时分就用AController.oneButtonClick
就能够了。
这儿有一个问题:
为什么我区分事务的时分,界说用的是enum AController
,而不是class AController
或许struct AController
?
咱们持续往下看。
已然我都是用枚举了,我还用static let
这样吗?我的enum去“继承”String不就好了吗?
enum AController: String {
case oneButtonClick
case twoButtonClick
.
.
.
}
或许
enum AController: String {
case oneButtonClick, twoButtonClick...
}
调用的时分我就用AController.oneButtonClick.rawValue
就行了。
假如遇到枚举值和字符串不同的时分,咱们特别处理一下就能够了:
enum AController: String {
case oneButtonClick
case twoButtonClick
case threeButtonClick = "3_button_click"
.
.
.
}
考虑到对全体的点击工作整合,咱们能够这样进行分类并调用ClickEvent.AController.oneButtonClick.rawValue
到这儿,我想各位应该懂了为啥要运用enum去界说点击工作了,经过声明enum的rawValue为String类型,能够让我少写一些不必要的代码。
enum ClickEvent {
enum AController: String {
case oneButtonClick
case twoButtonClick
.
.
.
}
enum BController: String {
case oneButtonClick
case twoButtonClick
.
.
.
}
.
.
.
}
这样的写法,让界说点击工作变得容易,可是调用的时分并不是特别友爱,我分了多个事务层,导致需求.
很屡次才干获取一个想要的字符串。所以说在这块咱们能够自行酌量。
并且这儿不是封装的最终阶段,咱们持续进行。
attributes入参封装
咱们都知道,传递字典是一个比较辛苦的工作,经过Alamofire传参的经验,恪守Codable协议的Model转Dictionary就行了。
所幸是,我所编写的App友盟点击工作的特点上传,Dictionary是单层的,并且Key和Value都是String。工作变得简略起来。
咱们先界说一个Model类型,小试牛刀:
struct AModel: Codabel {
let name: String
let webSite: String
}
extension AModel {
var toMap: [String: String] {
let encoder = JSONEncoder()
guard let data = try? encoder.encode(self) else {
return [:]
}
guard let dict = try? JSONSerialization.jsonObject(with: data) as? [String: String] else {
return [:]
}
return dict
}
}
调用的时分AModel(name: "season", webSite: "www.juejin.com").toMap
就能直接转成Dictionary啦。
这儿AModel用struct
界说,是由于struct
会依据界说的特点主动生成结构器,能够削减写init的代码。
可是假如我有别的一个BModel对应别的一个工作,就要把toMap
这个Copy一份,这样也太傻了吧。
当发现需求不断去机械Copy代码的时分,就需求考虑可不能够概括总结,封装一个办法了。
Swift是一门面向协议的编程言语,所以在考虑这种重复性代码的问题的时分,优先考虑用协议能不能解决呢?
说干就干,于是就编写了这样一个ToMapProtocol
:
protocol ToMapProtocol {
var toMap: [String: String] { get }
}
extension ToMapProtocol where Self: Codable {
var toMap: [String: String] {
let encoder = JSONEncoder()
guard let data = try? encoder.encode(self) else {
return [:]
}
guard let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return [:]
}
return dict
}
}
运用的只需恪守这个协议,经过协议的默许办法就能够转为Dictionary了:
struct AModel: Codable {
.
.
.
}
extension AModel: ToMapProtocol {}
struct BModel: Codable {
.
.
.
}
extension BModel: ToMapProtocol {}
.
.
.
这个时分调用友盟的计算点击的API大概就变成了这个姿态:
MobClick.event(ClickEvent.AController.oneButtonClick.rawValue, attributes: AModel(name: "season", webSite: "www.juejin.com").toMap)
这样看起来,甚至还没有最一开始的简练,可是在调用这个方面,这种办法无疑愈加高效与安全。
咱们封装到了最终了吗?没有!咱们接着持续。
做最终一次薄薄的封装,让调用变得愈加简略
咱们发现,在入参eventId的时分,咱们运用枚举有必要要调用.rawValue
,而入参attributes的时分,咱们有必要要调用.toMap
。
每次工作都这么写,就有点累。俗话说,不想偷闲的程序员不是好程序员,咱们来进行一层封装,少写几行代码吧:
func uploadEvent(event: String, model: ToMapProtocol? = nil) {
MobClick.event(event, attributes: model?.toMap ?? [:])
}
我在MobClick.event
上层封了一个uploadEvent
办法,特点入参不再是Dictionary,而是一个恪守ToMapProtocol
的目标,那么我只需在这个办法的内部调用一次model?.toMap
就能够了。
就像这样:
uploadEvent(ClickEvent.AController.oneButtonClick.rawValue, model: AModel(name: "season", webSite: "www.juejin.com"))
这个时分,有同事和我反馈了这样一个运用问题:
model需求传入恪守ToMapProtocol的目标固然是功德,可是有的时分,某个点击工作需求上传的特点就只有一对键值对,比方[“age”: “12”],为了一对键值对,我还要有必要创建一个模型,太费事了,能想想办法吗?
已然只需传入一个恪守ToMapProtocol
协议的目标就能够,那么我只需求让[String: String]
也恪守这个协议,并回来一个Dictionary就不就完事了吗?
这个完成反而不复杂:
extension Dictionary: ToMapProtocol where Key == String, Value == String {
var toMap: [String : String] { self }
}
关于一个Key和Value都是String的字典,回来它自己本身就好了。
这个分类重写ToMapProtocol
的办法的关键是在where
之后的束缚。
到此,[String : String]
与模型都被统和在ToMapProtocol
协议下面了。
现在,让咱们借着经过Protocol统和的思路,看看怎样来进一步封装eventId。
eventId入参的再封装,方案一(向上统和)
enum ClickEvent {
enum AController: String {
case oneButtonClick
case twoButtonClick
}
enum BController: String {
case oneButtonClick
case twoButtonClick
}
}
ClickEvent.AController
与ClickEvent.BController
被拆分的太细了,假如都统和到ClickEvent
下面的话就简略了:
enum ClickEvent: String {
/// 削减分层,都到ClickEvent这一层
/// A页面的事务
case AoneButtonClick
case AtwoButtonClick
/// B页面的事务
case BoneButtonClick
case BtwoButtonClick
}
于是乎终究的上层封装便是变成了这样:
func uploadEvent(clickEvent: ClickEvent, model: ToMapProtocol? = nil) {
MobClick.event(clickEvent.rawValue, attributes: model?.toMap ?? [:])
}
调用:
uploadEvent(ClickEvent.AoneButtonClick, model: AModel(name: "season", webSite: "www.juejin.com"))
eventId入参的再封装,方案二(经过Protocol统和)
依据ToMapProtocol
统和[String: String]
和Model
的经验,咱们大致能够这么构思代码:
protocol ToStringProtocol {
/// 这儿暂时不写详细完成
func abstractFunction() -> String
}
enum ClickEvent {
/// 让AController恪守ToStringProtocol
enum AController: String, ToStringProtocol {
case oneButtonClick
case twoButtonClick
}
/// 让BController恪守ToStringProtocol
enum BController: String, ToStringProtocol {
case oneButtonClick
case twoButtonClick
}
}
于是乎终究的上层封装的伪代码变成了这样:
func uploadEvent(aEvent: ToStringProtocol, model: ToMapProtocol? = nil) {
MobClick.event(aEvent.abstractFunction(), attributes: model?.toMap ?? [:])
}
ToStringProtocol协议里需求某个一个办法,能够将一个enum的状态值转为String就好啦。
那么ToStringProtocol协议里面的详细完成我就这么写:
protocol ToStringProtocol {
/// func abstractFunction() -> String变成了toString这个只读计算特点
var toString: String { get }
}
enum ClickEvent {
/// 让AController恪守ToStringProtocol
enum AController: String, ToStringProtocol {
case oneButtonClick
case twoButtonClick
var toString: String { /// 详细完成 }
}
/// 让BController恪守ToStringProtocol
enum BController: String, ToStringProtocol {
case oneButtonClick
case twoButtonClick
var toString: String { /// 详细完成 }
}
}
已经来到了最最最关键的一步,toString
的每个enum的完成怎样写?
还记得它吗——ClickEvent.AController.oneButtonClick.rawValue
,这不就简略了:
enum AController: String, ToStringProtocol {
case oneButtonClick
case twoButtonClick
var toString: String { rawValue }
}
最终的封装代码就成了这个姿态:
func uploadEvent(aEvent: ToStringProtocol, model: ToMapProtocol? = nil) {
MobClick.event(aEvent.toString, attributes: model?.toMap ?? [:])
}
调用:
uploadEvent(ClickEvent.AController.oneButtonClick, model: AModel(name: "season", webSite: "www.juejin.com"))
其实不管是方案一仍是方案二,目的都是共同的:
uploadEvent的这一层封装,让外部调用的时分,传入方便的参数就好,内部进行入参的转换与完成,避免外部写过多的重复代码。
这也是代码封装艺术的核心!!!
当然方案二的有一个问题便是,我没法经过extension ToStringProtocol where Self == enum {}
这样的where
办法去束缚enum
类型,不得不在每个enum
去完成协议办法,比较辛苦。
总结
就这样,一个API办法,在我的封装下面,写下了洋洋洒洒2000+字的文章。
其实很多人会有疑问,怎样样才干有这样的封装思想?
我依据自己的经验总结了几点:
1.当自己编写代码的时分,假如遇到经常需求CV的代码,是否停下来进行考虑与总结?
2.阅览优秀的开源源码,能够提升自己写代码的质量,同时理解大佬的思想形式。
3.封装并不是一次就能写得十分完美的,是反复打磨和实践出来的,趁一时之功,不如多考虑。
参考文档
Swift:网络请求库——Alamofire
Swift:where关键词运用
自己写的项目,欢迎咱们star⭐️
RxStudy:RxSwift/RxCocoa结构,MVVM形式编写wanandroid客户端。
GetXStudy:运用GetX,重构了Flutter wanandroid客户端。
本文正在参与「金石方案 . 分割6万现金大奖」