设计之美-Moya

阅读完 Moya 后, 对于 Moya 的感触就是函数式编程, 无处不在的面向协议编程以及插件化的思想.

Moya 的原理已经有很多文章都详细分析过了, 本文主要分析下 Moya 中那些值得学习借鉴的设计.


TargetType - 请求参数协议化

在官方示例中, 我们需要使用 Provider 来调用网络请求. 使用代码如下:

let provider = MoyaProvider<LoginAPI>()
provider.request(.login) { ... }
复制代码

非常简单明了的调用, 它也是调用 Moya 的唯一入口.

其中 LoginAPI 是一个遵循于 TargetType 协议的枚举类型, 用来定义网络请求中的基本参数和行为。

TargetType 协议如下:

public protocol TargetType {
    var baseURL: URL { get }
    var path: String { get }
    var method: Method { get }
    var parameters: [String: AnyObject]? { get }
    var sampleData: Data { get }
    ...
} 

// Implement a target
enum LoginAPI {
  enum authenticate
  enum login
}
extension LoginAPI: TargetType {
  var baseURL: NSURL{ return URL(string: "http://foo.cn") }
  var path: String {
    switch self {
      case authenticate: return "/authenticate"
      case login: return "/login"
    }
  }
  ...
}
复制代码

这种方式的好处显而易见, 完全利用面向协议的方式来定义网络请求的参数, 且使用枚举类型可以很方便地管理一组 API. 属性使用只读修饰, 更保证了值类型编程的状态可控性.

Provider 将按枚举类型从 LoginAPI 中获取参数, 并组合成一个 Request, 最后通过 Alamofire 发送.

Provider 构造函数 - 易用性与灵活性共存的函数式编程

先来看一下 Provider 的构造函数:

public init(endpointClosure: @escaping EndpointClosure = Provider.defaultEndpointMapping,
            requestClosure: @escaping RequestClosure = Provider.defaultRequestMapping,
            stubClosure: @escaping StubClosure = Provider.neverStub,
            callbackQueue: DispatchQueue? = nil,
            manager: Manager = Manager.default,
            plugins: [PluginType] = [],
            trackInflights: Bool = false)
复制代码

Provider 的构造函数包含了多个闭包. 简单看下各个参数的用处:

endpointClosure: 提供默认实现, 返回 TargetType 初始化的 Endpoint 对象, 也可以返回自定义的 Endpoint.

requestClosure: 由 Endpoint 获取 Request 进行网络请求的发送, 也可以自定义 Request 发送.

stubClosure: 决定启用何种 stub 策略, 可以在单元测试时开启.

plugins: 插件, 在下文中会详细说到它.

综上, 不难看出 Provider 真正做的事情就是:

既然 TargetType 已经具备了一个网络请求的所有参数, 那么为什么定义了 TargetType 后又要定义 EndpointClosureRequestClosure 呢? 这种看似累赘的设计, 其实是为了实现插件的功能.

首先官方建议将 TargetType 使用枚举类型来实现, 同时所有请求参数定义为只读, 这样对于一个请求, 它的初始参数将是唯一确定的, 严格控制了其状态.

同时, Moya 允许用户在 TargetType 生成 Endpoint 的过程和 Endpoint 生成 Request 的过程中通过闭包参数传递处理逻辑, 其中 EndpointClosure 允许用户在原来的 TargetType 基础上进行参数的修改, 比如在这个阶段临时添加一个 HTTPHeader 或者添加分页信息. RequestClosure 允许用户在 Endpoint 生成 URLRequest 的过程中拥有最后一次处理机会, 你可以在这个阶段修改 cookie,自定义头部字段等.

其实, 你在初始化 Provider 时不使用任何参数就可以满足日常需求, 它所提供的默认实现已经帮你完成了全部的转换. 而在用户有需求时, 可以在构造函数的闭包参数中插入修正代码, 这使得它非常灵活.

不需要额外处理的情况下, 用户可以忽略构造函数中的所有参数直接创建 Provider, 这大大提高了构造函数的易用性. 同时用户也可以在其中插入自定义的处理逻辑, 为构造函数提拱了足够的灵活性.

Stub - 易测试的网络层

单元测试中的一个重要概念就是 Stub(桩): 很多时候你需要的数据并不能简单地获取到, 它可能需要一个网络请求, 可能需要一次数据库查询, 也可能需要繁杂的构造条件, 所以我们需要使用 Stub 来提供默认的数据, 在需要时直接或延迟返回假数据即可.

Provider 中的 stubClosure 就是为单元测试提供的开关, 用户可以在 stubClosure 中返回需要的 stub 类型:

public enum StubBehavior {
    case never
    case immediate
    case delayed(seconds: TimeInterval)
}
复制代码

它所返回的假数据由 TargetTypeEndpoint 中的 sampleData 提供.

在构造函数中提供单元测试开关, 同时允许在构造函数内添加假数据, 使得 Moya 的网络请求测试变得异常简单.

Plugin - 插件化思想

TargetType->Endpoint->Request 的过程我们就看出了 Moya 插件化的思想, Plugin 更是将插件化实施落地, Provider 提供的插件参数是一个由遵循于 PluginType 的实例组成的集合, 它主要用于在网络请求的关键点进行事务处理. 我们先看下 PluginType 协议的内容:

public protocol PluginType {
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest
    func willSend(_ request: RequestType, target: TargetType)
    func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType)
    func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError>
}
复制代码

它定义了四个调用插件的节点, 下图是调用的流程图:

在四个 Plugin 节点, Moya 都会遍历 plugins 参数内的所有插件, 去执行各自的协议方法.

Moya 为常用的日志记录、加载时 loading 等功能添加了 NetworkLoggerPluginNetworkActivityPlugin 等四个可以直接使用的插件.

插件的做法像是将我们平时使用的代理回调抽象成了协议, 但这样做更利于扩展.

我们可以很方便地实现 PluginType 来自定义插件, 并实现对应的节点方法, 其他未实现的节点方法将会调用默认的扩展方法, 他们不会造成任何副作用. 这样的做法让网络层更加纯粹, 只注重于高层面的网络抽象, 将其他的处理逻辑用插件的方式开放给用户自定义.

Cancellable - 单一职责的请求取消协议

网络请求在很多场景下是需要取消的. 如: 在退出当前页面后, 当前页面正在进行的用于展示的数据请求应该被取消掉.

Moya 为每一个 request 函数提供了一个返回值, 返回值的类型是 Cancellable:

public protocol Cancellable {
    var isCancelled: Bool { get }
    func cancel()
}
复制代码

它只有一个只读属性和一个 cancel 方法, 自定义遵循 Cancellable 协议时, 建议将 isCancelled 属性声明为:

public private(set) var isCancelled: Bool = false
复制代码

可以确保 isCancelled 属性只在 cancel 方法内部被改变, 对外为只读.

调用 Cancellablecancel 方法最终是去调用 Alamofire.Request.cancel(), 但是返回 Cancellable 可以让类型的目的非常明确, 该返回值只用于网络请求的取消, 满足单一职责原则.

Result - 灵活的错误处理思想

Moya 依赖了两个开源库, 除了 Alamofire, 它还依赖了 Result.

Result 非常简单, 总共只有 400 行左右代码, 它提供了一个错误处理的思想: 正确时返回数据, 错误时返回错误类型, 官方示例:

// Definition
typealias JSONObject = [String: Any]

enum JSONError: Error {
    case noSuchKey(String)
    case typeMismatch
}

func stringForKey(json: JSONObject, key: String) -> Result<String, JSONError> {
    guard let value = json[key] else {
        return .failure(.noSuchKey(key))
    }
    if let value = value as? String {
        return .success(value)
    }
    else { 
        return .failure(.typeMismatch)
    }
}

// Usage
switch stringForKey(json, key: "email") {
case let .success(email):
    print("The email is \(email)")
case let .failure(.noSuchKey(key)):
    print("\(key) is not a valid key")
case .failure(.typeMismatch):
    print("Didn't have the right type")
}
复制代码

AFNetworking 提供了错误回调和成功回调分别对请求结果进行处理, 但实际使用中我们经常会在错误和成功后都执行某些代码或共享某些状态, 如果使用两个回调, 则必须写两份代码.

Result 和 AFNetworking 不一样, Result 则一次性返回正确错误结果, 用户可以使用 Switch 语句在一个作用域内处理结果, 更加灵活.

总结

Alamofire 已经近乎于是官方使用的网络框架, 在其上最热门的网络抽象库就是 Moya. 我想这完全得益于它满足了人们对于网络抽象层的需求: 可测试型, 易用性, 可扩展性.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值