许多开发者在长期维护各种代码库时面临的一个挑战是,如何将不同的框架和API整齐地连接起来,以适当地遵守所涉及的每种技术的惯例。
例如,随着世界各地的团队开始采用Swift 5.5的async/await-poweredconcurrency system,我们可能会发现自己需要创建与其他异步编程技术(如Combine)兼容的async-marked API版本的情况。
虽然我们已经看了Combine与异步序列和流等并发API的关系,以及我们如何能够在Combine管道中调用async-marked函数,但在本文中,让我们探讨如何能够轻松创建任何async API的基于Combine的变体,无论它是由我们定义的,还是由苹果定义的,还是作为第三方依赖的一部分。
异步期货
假设我们正在开发的一个应用程序包含以下ModelLoader ,它可以用来通过网络加载任何Decodable 模型。它通过一个看起来像这样的async 函数来执行其工作:
class ModelLoader<Model: Decodable> {
...
func loadModel(from url: URL) async throws -> Model {
...
}
}
现在,让我们假设我们也想创建一个基于Combine的上述loadModel API的版本,例如,为了能够在我们代码库的特定部分中调用它,这些部分可能是用Combine框架以更多的反应式方式编写的。
当然,我们可以选择专门为我们的ModelLoader 类型编写这种兼容性代码,但由于这是一个我们在使用基于Combine的代码时可能会多次遇到的普遍问题,让我们转而创建一个更通用的解决方案,我们将能够在我们的代码库中轻松重用。
由于我们正在处理async ,这些函数要么返回一个单一的值,要么抛出一个错误,让我们使用Combine的Future publisher来包装这些调用。这个发布者类型是专门为这类用例建立的,因为它给了我们一个闭包,可以用来向框架报告一个Result 。
所以让我们继续用一个方便的初始化器来扩展Future 类型,使之有可能用一个async 闭包来初始化一个实例。
extension Future where Failure == Error {
convenience init(operation: @escaping () async throws -> Output) {
self.init { promise in
Task {
do {
let output = try await operation()
promise(.success(output))
} catch {
promise(.failure(error))
}
}
}
}
}
创建这样一个抽象的力量,它与任何特定的用例无关,我们现在可以将它应用于任何我们想让 Combine 兼容的async API。只需要几行代码,在传递给我们新的Future 初始化器的闭包中调用我们想要连接的API--就像这样:
extension ModelLoader {
func modelPublisher(for url: URL) -> Future<Model, Error> {
Future {
try await self.loadModel(from: url)
}
}
}
很好!loadModel 请注意,我们可以选择给基于Combine的版本起一个与我们的async 的版本相同的名字(因为Swift支持方法重载)。然而,在这种情况下,将两者明确分开可能是个好主意,这就是为什么上述新API的名称中明确包含 "Publisher "一词。
反应式异步序列
异步序列和流也许是Swift标准库中最接近采用反应式编程的,这反过来又使这些API的行为与Combine非常相似--因为它们使我们能够随着时间的推移而发出数值。
事实上,在"异步序列、流和Combine "一文中,我们看了Combine发布器如何使用其values 属性直接转换为异步序列--但如果我们想反其道而行之,将一个异步序列(或流)转换为发布器呢?
继续之前的ModelLoader 的例子,假设我们的加载器类也提供了以下的API,它让我们创建一个AsyncThrowingStream ,发射一系列从URL阵列加载的模型。
class ModelLoader<Model: Decodable> {
...
func loadModels(from urls: [URL]) -> AsyncThrowingStream<Model, Error> {
...
}
}
就像以前一样,与其急着写代码将上述loadModels API转换为Combine发布器,不如试着想出一个通用的抽象,当我们想在项目中的其他地方写类似的桥接代码时,我们就可以重复使用。
这一次,我们将扩展Combine的PassthroughSubject 类型,这使我们能够完全控制它的值何时被发出,以及何时和如何终止。然而,我们不打算将这个API建模为一个方便的初始化器,因为我们要清楚地表明,调用这个API实际上将使创建的主体立即开始发射值。所以让我们把它变成一个静态工厂方法--像这样:
extension PassthroughSubject where Failure == Error {
static func emittingValues<T: AsyncSequence>(
from sequence: T
) -> Self where T.Element == Output {
let subject = Self()
Task {
do {
for try await value in sequence {
subject.send(value)
}
subject.send(completion: .finished)
} catch {
subject.send(completion: .failure(error))
}
}
return subject
}
}
有了上述内容,我们现在就可以像之前的async-marked API一样轻松地封装我们的基于流的loadModels API了--在这种情况下,唯一需要的额外步骤是将我们的PassthroughSubject 实例类型化为 AnyPublisher ,以防止任何其他代码能够向我们的主体发送新值。
extension ModelLoader {
func modelPublisher(for urls: [URL]) -> AnyPublisher<Model, Error> {
let subject = PassthroughSubject.emittingValues(
from: loadModels(from: urls)
)
return subject.eraseToAnyPublisher()
}
}
就这样,我们现在已经创建了两个方便的 API,使我们可以非常直接地使使用 Swift 并发系统的代码向后兼容 Combine--当与在某种程度上使用 Combine 的代码库一起工作时,这应该证明是非常方便的。
总结
尽管Swift现在确实有一个内置的并发系统,涵盖了与Combine相同的领域,但我认为这两种技术在未来几年内仍将非常有用--所以我们越能在它们之间建立顺畅的桥梁就越好。
虽然一些开发者可能会选择使用Swift并发性完全重写他们基于Combine的代码,但好消息是我们不需要这样做。只要有几个方便的API,我们就可以在这两种技术之间传递数据和事件,这反过来又可以让我们继续使用基于Combine的代码,即使我们开始采用async/await 和Swift的其他并发系统。
谢谢你的阅读!