到目前为止,我们已经构建了两个不同的加载器来处理身份验证,并且可以想象我们想要构建更多来支持其他加载器。 如果我们可以将所有“身份验证”逻辑封装到一个加载程序中,那不是很好吗?
我们将通过创建一个复合加载器来做到这一点。
设置
这个加载器在结构上与我们制作的其他加载器相似:大部分工作将发生在 load(task:) 方法中,我们也需要在某些时候实现 reset(with:) 。
public class Auth: HTTPLoader {
private let basic: BasicAuth = ...
private let oauth: OAuth = ...
public override func load(task: HTTPTask) {
if /* should load with basic auth */ {
basic.load(task: task)
} else if /* should load with oauth */ {
oauth.load(task: task)
} else {
super.load(task: task)
}
}
public override func reset(with group: DispatchGroup) {
basic.reset(with: group)
oauth.reset(with: group)
super.reset(with: group)
}
}
这看起来不错,对吧? 好吧,这不是😀。 这种方法有几个问题需要我们解决:
-
我们需要一种方法来确定哪个加载器应该加载特定请求。
-
reset(with:) 方法存在严重的逻辑缺陷。
为请求选择加载器
给定传入的 HTTPTask,我们需要一种方法来确定应该如何对其进行身份验证。 或者换句话说,每个请求都需要告诉我们它想要什么。 幸运的是,我们已经有办法指定每个请求的选项!
public struct AuthenticationMethod: Hashable, HTTPRequestOption {
// by default, requests are not authenticated
public static let defaultOptionValue: AuthenticationMethod? = nil
public static let basic = AuthenticationMethod(rawValue: "basic")
public static let oauth = AuthenticationMethod(rawValue: "oauth")
public let rawValue: String
public init(rawValue: String) { self.rawValue = rawValue }
}
extension HTTPRequest {
public var authenticationMethod: AuthenticationMethod? {
get { self[option: AuthenticationMethod.self] }
set { self[option: AuthenticationMethod.self] = newValue }
}
}
我选择使用 AuthenticationMethod 类型的结构来创建它,以便客户端可以创建自己的 AuthenticationMethods 以对应于他们自己的自定义身份验证加载器; 枚举会使这变得非常困难。 我们还可以更新加载器以允许客户端传递他们想要使用的身份验证加载器的种类,方法是创建一个 Dictionary<AuthenticationMethod, HTTPLoader> 将身份验证方法关联到特定的加载器:
public class Auth: HTTPLoader {
private let subloaders: [AuthenticationMethod: HTTPLoader]
public init(loaders: [AuthenticationMethod: HTTPLoader]) {
self.subloaders = loaders
super.init()
}
public override func load(task: HTTPTask) {
if let method = task.request.authenticationMethod {
// the request wants authentication
if let loader = subloaders[method] {
// we know which loader to use
loader.load(task: task)
} else {
// we don't know which loader to use
task.fail(.cannotAuthenticate)
}
} else {
// no authentication; immediately pass it on
super.load(task: task)
}
}
public override func reset(with group: DispatchGroup) {
subloaders.values.forEach { $0.reset(with: group) }
super.reset(with: group)
}
}
这样我们的 Auth loader 不仅支持内置的身份验证方法(BasicAuth 和 OAuth); 它还支持框架客户端制作的任何类型的 AuthenticationMethod 和自定义身份验证加载程序。 这种方法的另一个巨大优势是 Auth 加载器不需要拦截底层 BasicAuth 或 OAuth 加载器的任何委托方法。 由于这些加载器现在是在外部创建的,创建者可以将委托(用于读取/写入凭据等)设置为它想要的任何对象。 如果 Auth 加载器自己创建了这些值,我们将需要一种方法来注入委托或拦截委托方法调用,然后再通过不同的委托协议转发它们。
重置比赛
reset(with:) 方法有一个有趣的问题。 让我们看一下 nextLoader 值是如何用这种复合加载器配置的:
我们的内部加载器需要一个 .nextLoader 值,因为它们需要一种方法将修改后的任务向下传递到链中,以便最终通过网络传输。 然而,如果我们直接将它们发送到 Auth 加载器的 .nextLoader,那么我们最终会遇到这样一种情况,即下一个加载器将在一次重置尝试中多次调用其 reset(with:) 方法。 我们可以想象这种情况的发生:
Auth 收到重置调用并开始重置之前的加载程序。 它指示 Auth 重置 Auth 指示每个子加载器重置,并指示其下一个加载器重置 每个子加载器开始重置并指示其下一个加载器重置,但 Auth 已经告诉该加载器重置 因此,我们需要拦截来自内部加载器的所有这些重置调用,并阻止它们在链中传播。 幸运的是,我们已经有一个加载程序可以做到这一点! 回到第 9 部分,我们构建了一个 ResetGuard 加载器,它正是这样做的。
从广义上讲,Auth 加载器需要确保每个子加载器的 .nextLoader 都指向一个私有的 ResetGuard 加载器。 然后它需要拦截设置自己的 nextLoader 的调用,而不是使它成为 ResetGuard 的下一个加载器。 通过这种方式,Auth 加载器在整个加载链中注入一个新的 ResetGuard 加载器实例。
我会把这个留给你去实施。
结论
总的来说,构建复合加载器是一项概念上直接的任务。 关于我们需要处理的 nextLoader 有一个有趣的边缘案例,但是一旦我们理解了这个问题,解决方案就会出现我们已经构建的组件。
在下一篇文章中,我们将研究一种不同类型的复合加载器来支持 OAuth 的变体:OpenID。