Swift (iOS/macOS/Linux) 的声明式、易于使用且安全的依赖注入框架
特性
- 通过注解声明依赖(无需配置文件)
- 自动生成 DI 容器
- 依赖图编译时验证
- 支持 ObjC
- 非可选依赖解析
- 类型安全
- 带参数的注入
- 注册作用域
- DI 容器层次结构
- 线程安全
依赖注入
依赖注入基本上意味着“给对象其实例变量” ¹。它似乎并不是什么大事,但随着项目的规模变大,它就会变得复杂。构造函数变得过于复杂,通过多个层级传递依赖变得耗时,甚至仅仅找到依赖的来源都足够容易让人放弃,最终使用单例。
然而,依赖注入是软件开发架构的基本方面,没有理由不正确实现它。这就是 Weaver 可以帮助的地方。
什么是 Weaver?
Weaver 是一个为 Swift 提供的声明式、易于使用且安全的依赖注入框架。
- 声明式 因为它允许开发者在 Swift 代码中直接通过注解 声明依赖。
- 易于使用 因为它 生成必要的样板代码 以将依赖注入到 Swift 类型中。
- 安全 因为它 在编译时验证依赖图,并在有问题时输出美丽的 Xcode 错误。
如何使用Weaver?
尽管Weaver能够使依赖注入自行工作,了解它在底层做了什么也是很重要的。需要关注的有两个阶段:编译时间和运行时间。
在编译时
|-> link() -> dependency graph -> validate() -> valid/invalid
swift files -> scan() -> [Token] -> parse() -> AST -|
|-> generate() -> source code
Weaver的命令行工具扫描项目的Swift源代码,寻找注解,并生成一个AST(抽象语法树)。它使用SourceKitten,这是由苹果的SourceKit支持的,这使得这一步非常可靠。
然后使用这个AST生成一个依赖图,在这个图上执行一系列的安全检查,以确保代码在运行时不会崩溃。它会检查无法解决的依赖和不可解决的循环依赖。如果发现任何问题,则不会生成代码,这意味着项目将无法编译。
相同的AST还用于生成模板代码。它为每个具有可注入依赖的类/结构体生成一个依赖容器。它还生成一堆扩展和协议,以便将依赖注入对开发者几乎透明。
在运行时
Weaver实现了一个轻量级DI(依赖注入)容器对象,该对象能够根据范围、协议或具体类型、名称和参数来注册和解决依赖。每个容器可以有一个父容器,允许在容器层次结构中解决依赖。
当对象注册一个依赖关系时,其关联的DI容器存储一个构建器(和有时是一个实例)。当另一个对象声明对该同一依赖关系的引用时,其相关DI容器声明一个访问器,该访问器尝试解决依赖。解决依赖基本上意味着在回溯容器层次结构的过程中寻找构建器/实例。如果没有找到依赖关系,或者这个过程陷入无限递归,它将在运行时崩溃,这就是为什么在编译时检查依赖图非常重要的原因。
安装
Weaver 包含 3 个部分
- Swift 框架,可包含到您的项目中
- 命令行工具,用于在您的机器上安装
- 构建阶段,可添加到您的项目中
(1) - Weaver 框架安装
Weaver 的 Swift 框架可以通过 CocoaPods
、Carthage
和 SwiftPM
使用。
CocoaPods
在 Podfile
中添加 pod 'WeaverDI', '~> 0.9.11'
。
Carthage
在 Cartfile
中添加 github "scribd/Weaver" ~> 0.9.11
。
SwiftPM
在 Package.swift
文件的依赖部分添加 .package(url: "https://github.com/scribd/Weaver.git", from: "0.9.11")
。
(2) - Weaver 命令行工具安装
可以使用 Homebrew
或手动方式安装 Weaver 命令行工具。
二进制形式
从发布标签页下载预构建的二进制文件。将存档解压到目标位置,并运行bin/weaver
Homebrew
在其添加到主Homebrew存储库之前,可按如下方式安装Weaver
$ brew tap trupin/homebrew-core
$ brew install weaver
从源码构建
从发布标签页下载最新发布的源代码或克隆存储库。
在项目目录中,运行brew update && brew bundle && make install
以构建和安装命令行工具。
检查安装
运行以下命令以检查Weaver是否已正确安装。
$ weaver --help
Usage:
$ weaver <input_paths>
Arguments:
input_paths - Swift files to parse.
Options:
--output_path [default: .] - Where the swift files will be generated.
--template_path - Custom template path.
--unsafe [default: false]
(3) - Weaver构建阶段
在Xcode中,将以下命令添加到命令行构建阶段
weaver --output_path ${SOURCE_ROOT}/output/path `find ${SOURCE_ROOT} -name '*.swift' | xargs -0`
注意 - 将此构建阶段移动到编译源代码
阶段之上,这样Weaver就可以在编译之前生成样板代码。
警告 - 不推荐使用--unsafe
。这将禁用图验证,这意味着生成的代码如果依赖图无效可能会崩溃。只有在图验证阻止项目编译,尽管它不应该时,才将其设置为false。如果您遇到这种情况,请随时提交一个错误报告。
基本用法
要查看更完整的用法示例,请查看示例项目。
让我们实现一个显示电影列表的非常基础的程序。它将由三个明显的对象组成
AppDelegate
在这里注册依赖。MovieManager
提供电影。MoviesViewController
在屏幕上显示电影列表。
让我们来看看代码。
AppDelegate
:
import UIKit
import Weaver
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private let dependencies = AppDelegateDependencyContainer()
// weaver: movieManager = MovieManager <- MovieManaging
// weaver: movieManager.scope = .container
// weaver: moviesViewController = MoviesViewController <- UIViewController
// weaver: moviesViewController.scope = .container
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow()
let rootViewController = dependencies.moviesViewController
window?.rootViewController = UINavigationController(rootViewController: rootViewController)
window?.makeKeyAndVisible()
return true
}
}
AppDelegate
注册了两个依赖
// weaver: movieManager = MovieManager <- MovieManaging
// weaver: moviesViewController = MoviesViewController <- UIViewController
这些依赖因为它们的范围设置为container
,所以可以被来自AppDelegate
的任何对象访问
// weaver: movieManager.scope = .container
// weaver: moviesViewController.scope = .container
依赖注册会自动在AppDelegateDependencyContainer
中生成注册代码和一个访问器,这就是为什么可以构建rootViewController
let rootViewController = dependencies.moviesViewController
.
MovieManager
:
protocol MovieManaging {
func getMovies(_ completion: @escaping (Result<Page<Movie>, MovieManagerError>) -> Void)
}
final class MovieManager: MovieManaging {
func getMovies(_ completion: @escaping (Result<Page<Movie>, MovieManagerError>) -> Void) {
// fetches movies from the server...
completion(.success(movies))
}
}
MoviesViewController
:
final class MoviesViewController: UIViewController {
private let dependencies: MoviesViewControllerDependencyResolver
private var movies = [Movie]()
// weaver: movieManager <- MovieManaging
required init(injecting dependencies: MoviesViewControllerDependencyResolver) {
self.dependencies = dependencies
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
// Setups the tableview...
// Fetches the movies
dependencies.movieManager.getMovies { result in
switch result {
case .success(let page):
self.movies = page.results
self.tableView.reloadData()
case .failure(let error):
self.showError(error)
}
}
}
// ...
}
MoviesViewController
声明一个依赖引用
// weaver: movieManager <- MovieManaging
这个注解在MoviesViewControllerDependencyResolver
中生成一个访问器,但没有注册,这意味着MovieManager
不会存储在MoviesViewControllerDependencyContainer
中,而是在其父级中,即构建它的容器。在这种情况下是AppDelegateDependencyContainer
。
MoviesViewController
还需要声明一个特定的初始化器
required init(injecting dependencies: MoviesViewControllerDependencyResolver)
这个初始化器用于注入DI容器。请注意,MoviesViewControllerDependencyResolver
是一个协议,这意味着可以在测试时注入假的DI容器版本。
API
代码注解
Wheaver 允许您通过使用类似这样的注释 // weaver: ...
来声明依赖。
目前它支持以下注解。
- 依赖注册注解
- 将依赖构建器添加到容器中。
- 将依赖的访问器添加到容器的解析器协议中。
示例
// weaver: dependencyName = DependencyConcreteType <- DependencyProtocol
或
// weaver: dependencyName = DependencyConcreteType
dependencyName
:依赖项的名称。用于在其他对象和/或注解中引用依赖项。DependencyConcreteType
:依赖项的实现类型。可以是struct
或class
。DependencyProtocol
:如果有,依赖项的protocol
。可选,您可以仅使用具体类型注册依赖项。
- 作用域注解
设置依赖项的作用域。默认作用域为 graph
。仅与注册注解一起使用。
scope
定义了依赖项的访问级别和缓存策略。有以下四个作用域:
transient
:每次解析时始终创建一个新实例。不能从子对象访问。graph
:第一次解析时创建一个新实例,然后在整个容器生命周期中存在。不能从子对象访问。weak
:第一次解析时创建一个新实例,然后在其强引用存在的时间内存在。可以从子对象访问。container
:类似于 graph,但可以从子对象访问。
示例
// weaver: dependencyName.scope = .scopeValue
scopeValue
:作用域的值。可以是上述描述中的一种值。
- 依赖项引用注解
向容器的协议中添加对依赖项的访问器。
示例
// weaver: dependencyName <- DependencyType
DependencyType
:依赖项的具体系列或抽象系列。这还定义了依赖项访问器返回的类型。
- 自定义引用注解
将方法 dependencyNameCustomRef(_ dependencyContainer:)
添加到容器的解析 protocol
中。默认值是 false
。Weaver 会保留此方法未实现,这意味着您需要自行实现它并手动解析/构建依赖项。
与注册和引用注解一起使用。
警告 - 确保您不要在此方法中执行任何不安全操作,因为 dependencyContainer
参数不会被依赖项图验证器捕获。
示例
// weaver: dependencyName.customRef = aBoolean
aBoolean
:布尔值,定义依赖项是否应具有自定义引用。可以取 true
或 false
的值。
- 参数注释
向容器解析协议添加一个参数。这意味着生成的容器需要在初始化时获取这些参数。这也意味着所有相关的依赖访问器都需要获取这个参数。
示例
// weaver: parameterName <= ParameterType
- 配置注释
为相关的对象设置配置属性。
示例
// weaver: self.attributeName = aValue
配置属性
isIsolated: Bool
(默认值:false
): 任何将此设置为true的对象都被Weaver视为项目未使用的对象。标记为孤立的对象只能有孤立的后代。此属性对开发不包含项目中所有依赖的功能很有用。
阅读更多...
致谢
Weaver的DI容器功能受到了Swinject的启发。
参与贡献
- 克隆项目
- 创建您的功能分支(
git checkout -b my-new-feature
) - 提交您的更改(
git commit -am 'Add some feature'
) - 推送到分支(
git push origin my-new-feature
) - 创建一个新的拉取请求
许可证
MIT许可证。有关详细信息,请参阅LICENSE文件。