到目前为止,我们创建的 HTTPLoading
类型都是直接响应 HTTPRequest
的加载器。 为了创建新类型的加载器,我们需要重新访问 HTTPLoading
协议。
public protocol HTTPLoading {
func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void)
}
如果回想第 4 部分,我们会记得我们对协议进行了以下观察:
我们发送一个请求,在未来的某个时刻,我们的闭包将根据我们收到的任何响应执行。
我想在这里指出,这个定义没有说明如何检索响应。 这种区别就是我们可以在上一篇文章中创建 MockLoader
的原因。 这个定义也没有说明这个特定的加载器创建响应。 所有这个协议定义的是,如果我们用一个请求调用这个方法,在未来的某个时候,完成块将被执行并得到一个结果。
那么……如果我们不调用此加载器中的完成块,而是允许另一个加载器调用它呢? 我们仍然会满足 API
合同的条款(请求进来,完成最终得到执行)。
更具体地说,让我们想象一个 AnyLoader
类型:
class AnyLoader: HTTPLoading {
private let loader: HTTPLoading
init(_ other: HTTPLoading) {
self.loader = other
}
func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
loader.load(request: request, completion: completion)
}
}
即使这个加载器本身不执行完成块,它仍然满足成为“HTTPLoading
”类型的所有要求。 这种让加载器将加载责任委托给其他加载器的能力是我们框架功能的核心。 它将允许我们组成一个自定义的加载程序序列来执行请求。 我们将把这个“下一个加载器”的概念形式化为我们协议的一部分:
public protocol HTTPLoading {
var nextLoader: HTTPLoader? { get set }
func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void)
}
我们希望在所有 HTTPLoading
实例上有一些共同的逻辑。 例如,如果您已经设置了 nextLoader
一次,那么您应该无法更改它,这似乎是合理的。 此外,如果您尝试在没有 nextLoader
的情况下加载请求,则请求失败似乎是合乎逻辑的。 因此,我们将 HTTPLoading
更改为一个类,我们可以在其中封装所有加载程序通用的逻辑:
open class HTTPLoader {
public var nextLoader: HTTPLoader? {
willSet {
guard nextLoader == nil else { fatalError("The nextLoader may only be set once") }
}
}
public init() { }
open func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
if let next = nextLoader {
next.load(request: request, completion: completion)
} else {
let error = HTTPError(code: .cannotConnect, request: request)
completion(.failure(error))
}
}
}
这对我们有什么影响可能不是很明显,所以让我们创建另一个加载程序,在请求执行时打印请求:
public class PrintLoader: HTTPLoader {
override func load(request: HTTPRequest, completion: @escaping (HTTPResult) -> Void) {
print("Loading (request)")
super.load(request: request, completion: { result in
print("Got result: (result)")
completion(result)
})
}
}
使用这个和另一个类似的加载器来包装 URLSession
,我们可以构建这个链:
let sessionLoader = URLSessionLoader(session: URLSession.shared)
let printLoader = PrintLoader()
printLoader.nextLoader = sessionLoader
let loader: HTTPLoader = printLoader
现在,每当我们使用我们的加载程序值执行网络请求时,每个请求都会在 URLSession
上执行之前被记录下来,并且每个响应都会在它返回后被记录下来。
自定义操作符
这是我认为定义自定义运算符可能值得的罕见情况之一。 如果我们最终遇到的情况是我们有多个加载器要链接在一起,那么定义该链可能会非常冗长。 “箭头”运算符使这个定义更简单:
precedencegroup LoaderChainingPrecedence {
higherThan: NilCoalescingPrecedence
associativity: right
}
infix operator --> : LoaderChainingPrecedence
@discardableResult
public func --> (lhs: HTTPLoader?, rhs: HTTPLoader?) -> HTTPLoader? {
lhs?.nextLoader = rhs
return lhs ?? rhs
}
首先,我们将定义一个自定义优先级组,以定义我们的相对操作顺序。 我们定义的优先级组表明我们的优先级高于“零合并”,但低于接下来的任何优先级。 它还定义了我们具有“正确的结合性”。 当具有相同优先级的多个运算符出现在同一语句中时,运算符的“关联性”告诉编译器要遵循的顺序。
例如,如果我们有语句 a <op> b <op> c
并且 <op>
是左关联的,那么 a <op> b
将在 ... <op> c
之前执行。 但是,如果它是右结合的,那么 b <op> c
将首先被执行,然后结果将被用作 a <op>
的右侧......
接下来,我们为运算符 (-->) 定义符号和实现它的函数。 由于我们已将运算符定义为右结合,因此我们更愿意返回运算符左侧的值作为结果。
结合性很重要,因为我们希望如何使用此运算符:
let chain = loaderA --> loaderB --> loaderC --> loaderD
如果 -->
是左关联的,那么这将计算从左到右并等效于:
loaderA.nextLoader = loaderB
loaderB.nextLoader = loaderC
loaderC.nextLoader = loaderD
chain = loaderD
这看起来大部分是正确的,除了我们最终在链变量中得到了错误的值。
另一方面,通过使它成为右结合运算符,这段代码将从右到左求值:
loaderC.nextLoader = loaderD
loaderB.nextLoader = loaderC
loaderA.nextLoader = loaderB
chain = loaderA
所有的加载器仍然像我们预期的那样被连接起来,但我们最终将“第一个”加载器作为链变量的值。
链式加载器
创建加载器链的能力是我们最终要添加到该框架的所有功能的基础。 它允许我们创建大规模可组合的网络堆栈,链中的各个“链接”执行专门的任务。 我们可以动态定义链; 我们将有能力完全跳过链的某些部分; 我们甚至可以将链分布到不同的设备上。
在下一篇文章中,我们将构建一个加载程序,允许我们动态更改正在运行的请求。