本文正在参与「金石方案 . 分割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.AControllerClickEvent.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万现金大奖」