依赖项
一个受 SwiftUI "environment" 启发的依赖管理库。
了解更多
这个库的动机和设计过程贯穿于许多集 Point-Free 上, 这是一个由 Brandon Williams 和 Stephen Celis 主持的视频系列,探索函数式编程和 Swift 语言。
概述
依赖项是应用程序中需要与您无法控制的外部系统交互的类型和函数。经典的例子包括向服务器发出网络请求的 API 客户端,但也包括看似无关的东西,如 UUID
和 Date
初始化器、文件访问、用户默认值,甚至时钟和计时器,都可以视为依赖项。
在应用程序开发中,您可以走得很远而不需要考虑依赖管理(或者,有些人喜欢称之为“依赖注入”),但最终,不受控制的依赖会在您的代码库和开发周期中引发许多问题:
-
不受控制的依赖使得编写快速、确定性的测试变得困难,因为您容易受到外部世界的变幻莫测的影响,例如文件系统、网络连接、互联网速度、服务器正常运行时间等。
-
许多依赖项在 SwiftUI 预览中不起作用,例如位置管理器和语音识别器,有些甚至在模拟器中也不起作用,例如运动管理器等。如果您使用这些框架,这将阻止您轻松地迭代功能设计。
-
与第三方、非 Apple 库(如 Firebase、web socket 库、网络库等)交互的依赖项往往重量级且编译时间较长。这会减慢您的开发周期。
由于这些原因,以及更多其他原因,我们强烈建议您控制您的依赖,而不是让它们控制您。
但控制依赖只是开始。一旦您控制了依赖,还会面临一系列新的问题:
-
如何传播依赖到整个应用程序,比显式地到处传递它们更符合人体工学,但比全局依赖更安全?
-
如何仅为应用程序的一部分覆盖依赖? 这在为测试和 SwiftUI 预览覆写依赖以及特定用户流(如入门体验)时非常有用。
-
您如何确保在测试中覆盖了某一功能所使用的所有依赖? 对于测试来说,模拟一些依赖而让其他依赖与外部世界交互是不正确的。
这个库解决了上述所有问题,甚至更多。
快速开始
该库允许您注册自己的依赖项,但它也附带许多可控的依赖项(参见 DependencyValues
了解完整列表),您很可能可以立即使用。如果您在功能逻辑中直接使用 Date()
、UUID()
、Task.sleep
或 Combine 调度器,您已经可以开始使用该库了。
final class FeatureModel: ObservableObject {
@Dependency(\.continuousClock) var clock // 可控的任务休眠方式
@Dependency(\.date.now) var now // 可控地获取当前日期
@Dependency(\.mainQueue) var mainQueue // 可控地调度主队列
@Dependency(\.uuid) var uuid // 可控的 UUID 创建
// ...
}
一旦声明了依赖,不要直接使用 Date()
、UUID()
等,而是使用在功能模型上定义的依赖:
final class FeatureModel: ObservableObject {
// ...
func addButtonTapped() async throws {
try await self.clock.sleep(for: .seconds(1)) // 👈 不要使用 'Task.sleep'
self.items.append(
Item(
id: self.uuid(), // 👈 不要使用 'UUID()'
name: "",
createdAt: self.now // 👈 不要使用 'Date()'
)
)
}
}
这就是在您的功能中使用可控依赖所需的一切。通过这点点前期工作,您可以开始利用库的强大功能。
例如,您可以在测试中轻松控制这些依赖。如果您想测试 addButtonTapped
方法中的逻辑,可以使用 withDependencies
函数在单个测试范围内覆盖任何依赖。这就像 1-2-3 一样容易:
func testAdd() async throws {
let model = withDependencies {
// 1️⃣ 覆盖功能使用的任何依赖。
$0.clock = ImmediateClock()
$0.date.now = Date(timeIntervalSinceReferenceDate: 1234567890)
$0.uuid = .incrementing
} operation: {
// 2️⃣ 构建功能的模型
FeatureModel()
}
// 3️⃣ 现在该模型在一个受控的依赖环境中执行,
// 因此我们可以对其行为做出断言。
try await model.addButtonTapped()
XCTAssertEqual(
model.items,
[
Item(
id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!,
name: "",
createdAt: Date(timeIntervalSinceReferenceDate: 1234567890)
)
]
)
}
这里我们控制了 date
依赖总是返回相同日期,并控制 uuid
依赖每次被调用时返回自增的 UUID,我们甚至使用 ImmediateClock
来控制 clock
依赖,将所有时间压缩到一个瞬间。如果我们不控制这些依赖,这个测试将很难编写,因为无法准确预测 Date()
和 UUID()
返回什么,并且我们必须等待现实世界的时间流逝,这使得测试变慢。
但是,可控依赖不仅对测试有用。它们也可以在 Xcode 预览中使用。假设上述功能使用一个时钟在视图中进行某段时间的休眠。如果您不想实际等待时间流逝才能看到视图如何变化,可以使用 withDependencies
辅助工具将时钟依赖覆盖为“即时”时钟:
struct Feature_Previews: PreviewProvider {
static var previews: some View {
FeatureView(
model: withDependencies {
$0.clock = ImmediateClock()
} operation: {
FeatureModel()
}
)
}
}
这将使得预览在运行时使用即时时钟,但在模拟器或设备上运行时仍然使用实时 ContinuousClock
。这样可以只为预览覆盖依赖,而不影响应用程序在生产中的运行。
这是开始使用该库的基本步骤,但你还能做更多的事情。你可以通过查看文档和文章更深入地了解该库:
入门
基础
-
使用依赖项:
学习如何使用在该库中注册的依赖项。 -
注册依赖项:
了解如何在库中注册你自己的依赖项,使其可以立即在代码库的任何部分使用。 -
实时、预览和测试依赖项:
学习如何为实时应用程序、Xcode 预览甚至测试提供不同的依赖项实现。 -
测试:
控制依赖项的主要原因之一是为了更容易进行测试。学习一些使用该库编写更好测试的技巧。
高级
-
设计依赖项:
学习如何设计你的依赖项,使其在注入功能和测试覆盖时最具灵活性。 -
覆盖依赖项:
了解如何在运行时更改依赖项,以便应用程序的某些部分可以使用不同的依赖项。 -
依赖项生命周期:
了解依赖项的生命周期,如何延长依赖项的生命周期,以及依赖项如何继承。 -
单入口系统:
了解“单入口”系统,以及为什么它们最适合这个依赖项库,尽管也可以将该库用于非单入口系统。
示例
我们使用现代的 SwiftUI 开发最佳实践重建了 Apple 的 Scrumdinger 演示应用程序,包括使用该库控制文件系统访问、计时器和语音识别 API 的依赖项。该演示可以在此处找到。
文档
最新的 Dependencies API 文档可在此处获取。
安装
你可以通过将 Dependencies 作为包添加到项目中来将其添加到 Xcode 项目中。
如果你想在 SwiftPM 项目中使用 Dependencies,只需将其添加到 Package.swift
文件中:
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0")
]
然后将该产品添加到任何需要访问库的目标中:
.product(name: "Dependencies", package: "swift-dependencies"),
社区
如果你想讨论这个库或有关于如何使用它解决特定问题的问题,有多个地方可以与你的 Point-Free 爱好者交流:
- 对于长篇讨论,我们推荐此仓库的讨论标签。
- 对于随意聊天,我们推荐 Point-Free Community Slack。
扩展
该库开箱即控了许多依赖项,但也开放给扩展。以下项目都基于 Dependencies 进行构建:
- Dependencies Additions:
提供高级依赖项的附加库。 - Dependencies Protocol Extras:
使 swift-dependencies 在使用协议时更有用的库。
替代品
Swift 社区中还有许多其他依赖项注入库。每个库都有自己的一套优先级和权衡,这与 Dependencies 不同。以下是几个知名的例子:
许可证
该库是根据 MIT 许可证发布的。详见 LICENSE。