使用 protocol 优雅处理 API

最近在迁移到 Swift3.0 过程中,为了逐步将 AFNetworking 转移到 Alamofire 上,对于部分老的 OC 代码顺便一起做了重构,遂对于如何更好的组织 API 有了很多想法。

Why

在人员流动与 Swift 的版本更新过程中,项目中逐渐有了以下对网络操作的方式:

  • 基于 AFNetworking 的 OC 封装
  • 基于 AFNetworking 的 Swift 封装
  • 基于 Alamofire 的直接调用
  • 基于 Alamofire 的 Swift 封装

于是乎,同一个 API 可能有四种+的出现方式,加上同一个接口在不同服务中的调用,简直令人奔溃。

可以复用的东西当然要抽象出来,But,How?

How

Alamofire 4.0 版本中有很多变化: Request又进一步添加了 4 个子类 DataRequest, DownloadRequest, UploadRequest 以及 StreamRequest
pathmethod 对换了一个位置,ParameterEncoding 由枚举变成了协议。于是乎原来基于 Alamofire 的直接调用的就全挂了,修改的工程量浩大。所以,我选择基于 Alamofire 的 Swift 封装重新来组织 API。

Swift 推荐你使用更多的值类型,但有时候会没有对象那么灵活,基于我们的业务逻辑,我选择如下的 protocol ,具体的实现可以选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protocol APIType {
var baseURL: String { get }
var path: String { get }
var url: String { get }
var method: HTTPMethod { get }
var parameters: Parameters { get }
var encoding: ParameterEncoding { get }
var headers: [String : String] { get }
}

extension APIType {
var url: String { return baseURL + path }
var parameters: Parameters { return Parameters() }
var encoding: ParameterEncoding { return URLEncoding() }
var headers: [String : String] { return [:] }
}

由于 API 来自几台不同的服务器,同组的 API 有着相似的行为模式和错误处理方式,在这里定义好 baseURL, 同时也可以在这一层控制线上与线下环境,

1
2
3
4
5
6
7
8
9
10
11
protocol AnotherenAPIType: APIType { }

extension AnotherenAPIType {
var baseURL: String {
if EnvironmentManager.isOnline {
return "https://release.anotheren.com"
} else {
return "https://debug.anotheren.com"
}
}
}

为了方便调用,使用一个收集组来收集所有的同组 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct AnotherenAPI { }

extension AnotherenAPI {
struct Login: AnotherenAPIType {
let userName: String
let password: String
init(userName: String, password: String) {
self.userName = userName
self.password = password
}
var path: String { return "/login" }
var method: HTTPMethod { return .post }
var parameters: Parameters {
return ["userName" : userName,
"password" : password]
}
}
}

Advance

实际上对 API 返回数据的处理也是固定的,比如这个接口是按照 JSON 格式返回数据的,那其实就可以直接把数据处理后再返回,此处定义一个 associatedtype ResultType 让具体 API 定义时再确定 ResultType 的实际类型

1
2
3
4
5
6
7
8
9
10
protocol JSONAPIType: APIType {
associatedtype ResultType
func handleJSON(json: JSON) -> Alamofire.Result<ResultType>
}

extension AnotherenAPI.Login: JSONAPIType {
func handleJSON(json: JSON) -> Result<LoginInfo> {
...
}
}

最后具体使用的时候就可以这样处理,在回调的闭包中就可以直接拿到请求的结果 ResultType,类似的,当接口返回全是图片的时候也可以再增加 PictureAPIType 协议,并让相关接口实现即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func request(_ api: APIType) -> DataRequest {
let fullParameters = appendCustom(parameters: api.parameters)
let fullHeaders = appendCustom(headers: api.headers)
return Alamofire.request(api.url, method: api.method, parameters: fullParameters, encoding: api.encoding, headers: fullHeaders)
}

func requestJSON<T: JSONAPIType>(_ api: T, _ completionHandler: @escaping (Result<T.ResultType>) -> Void) -> DataRequest {
return request(api).responseSwiftyJSON({ result in
switch result {
case .success(let json):
completionHandler(api.handleJSON(json: json))
case .failure(let error):
completionHandler(Result.failure(error))
}
})
}

实际上,当把 API 层这样独立拆开后,测试起来也是非常方便的。