什么是 Injection
Injection 是一个简单的依赖注入库,允许您定义组件、模块,并为您解析依赖。
所有项目都基于一个原则。 编写更少
- 现代的 Swift 代码。
- 允许您按
Components
将依赖分组。 - 唯一作用域,每次调用
resolve
方法都会创建一个新的实例。 - 单例作用域,在整个
Module
中共享相同的实例。 - 弱作用域,当它存在时共享相同的实例,如果不是,则生成一个新实例。
- 如果您需要这样做,请使用标签标记您的依赖项。
- 解析可选类型。
- 循环依赖。
- 检查模块依赖项不会在运行时崩溃。
- 更多功能即将添加...
以下示例将简要介绍最简单(也是最常见)的情况
//lets start defining a component for our storage things.
let storageComponent = Component {
factory { Storage() }
}
//lets create other Component for use cases
let useCasesComponent = Component {
factory { UseCaseWithStorage(storage: $0()) }
}
//lets create a Module to get our dependencies
let module = Module {
component { storageComponent }
component { useCasesComponent }
}
//now you can get your dependencies using:
let useCase: UseCaseWithStorage = module.resolve()
//or
let useCase = module.resolve() as UseCaseWithStorage
让我们使其更出色。
当我们启动应用程序时,我们必须向 Injection 提供我们想要在代码中共享的模块,为此,我们需要使用 injectMe(yourModuleHere)
函数调用所需的模块。另外,为了更简洁,我们可以这样做
injectMe {
component { yourComponent }
factory { YourMagic() }
}
您可以在那里调用 injectMe
并创建您的模块。
在现代 iOS 应用程序中,模块以一个 RootThing
开始,让我们假设它是 ViewController
(或 Coordinator
,您在应用程序中使用什么)。
Injection 提供了一个名为 ModuleBuilder<T>
的新组件,其中 T
是您将使用的 RootThing
。此 ModuleBuilder
可以访问您之前通过调用 injectMe
提供的共享模块,我们还选项扩展此模块,提供一个新的 Component
。这个组件就在这里帮助您创建 RootThing
。
让我们看看最简单的情况在实际中的应用
final class YourScreenModuleBuilder: ModuleBuilder<UIViewController> {
override func build() -> UIViewController {
YourScreenViewController(dependency: module.resolve())
}
}
如果您需要扩展共享模块
final class YourScreenModuleBuilder: ModuleBuilder<UIViewController> {
override func component() -> Component? {
Component {
factory { YourCustomDependency() }
}
}
override func build() -> UIViewController {
YourScreenViewController(dependency: module.resolve(), custom: module.resolve())
}
}
然后,在需要创建屏幕时
//lets say you are pushing this screen:
navigationController.push(YourScreenModuleBuilder().build())
与 Navigator Pattern(例如 J&G 的员工使用的模式)高度兼容,因为我们可以在 屏幕扩展
上封装所有这些 VC 创建。
//We pass from this
extension Screen {
static func job(id: String) -> Self {
return .init() { JobViewController(id: id) }
}
}
//To
extension Screen {
static func job(id: String) -> Self {
return .init() { JobModuleBuilder(id: id).build() }
}
}
为什么不使用运行时参数?
其他库提供了运行时参数,为什么这个不提供?
运行时参数并未直接提供,让我来解释。
在通常的动态参数情况下(例如 module.resolve(parameter: "parameter"
),你将失去类型安全、参数名、顺序,并且受限于库提供的最大值。这不是一个好的选择。此外,如果你有一个具有运行时参数的 2 层或更多层的依赖图,你需要在整个图中传递所有那些参数。
示例:
//This is pseudocode
struct A {
let parameter: String
}
struct B {
let a: A
}
struct C {
let b: B
}
//If you need C, you must share the parameter among all dependencies
//C dependends on B that depends on A that have a runtime param so...
container.register() { (parameter: String) in A(parameter: parameter) }
container.register() { (parameter: String) in B(a: container.resolve(arguments: parameter)) }
container.register() { (parameter: String) in C(b: container.resolve(arguments: parameter)) }
这不是类型安全,不是命名,当你键入注册 B 或 C 时,你不知道 A 需要多少个参数。主要观点是,像这种情况下的 "CoreComponents",它们不应该在运行时具有动态参数。
最常用的例子之一是,可能是具有在运行时提供的 URL 的 HTTPClient,你可能必须使用一个 Environment
对象,该对象在运行时提供 URL。示例:
let networkComponent = Component {
factory { HTTClient(url: Environment.current.hostURL) }
}
如果你有多个 URL,请使用带标签的工厂来使用不同的 URL。
let networkComponent = Component {
//app usual client
factory { HTTClient(url: Environment.current.hostURL) }
//google addresses client
factory(tag: "address") { HTTClient(url: "googleaddressurl") }
}
然而,如果你的组件仍然需要运行时参数,那么你还有很多其他选择。
ModuleBuilders 有望提供。
使用模块构建器,你可以选择自定义 init
方法。这意味着你可以提供 真正的运行时参数,带有命名和类型安全。然后,你可以扩展 injectMe
提供的 Injection
模块,并使用自定义组件,然后神奇的事情发生了...
final class ScreenModuleBuilder: ModuleBuilder<UIViewController> {
private let parameter: String
init(parameter: String) {
self.parameter = parameter
}
override func component() -> Component? {
Component {
factory { YourAwesomeObject(parameter: self.parameter, dependency: $0()) }
}
}
override func build() -> UIViewController {
ScreenViewController(awesomeObject: module.resolve())
}
}
我们强烈建议您的共享模块实例不依赖于自定义组件的依赖项,因为您需要在所有模块构建器上提供它们,并且如果您忘记它,运行时可能会发生崩溃。
无论如何,最好如果您的 "CoreComponents" 不需要在初始化时动态参数。
测试
父模块会在这里帮你很多。您可以通过创建一个覆盖生产模块的 TestModule 并使用模拟依赖项,并具有使用真实组件和模拟组件的选项。
请记住,模块解析链将首先尝试解析自己的依赖项,然后如果找不到实例,将调用其父模块。这为我们提供了使用父依赖项来替换真实组件的能力。
更多技巧
- 我如何创建一个单例?
single { YourSingleton() }
- 我如何创建一个弱单例?
weak { YourClass() }
注意:如果没有在内存中保留实例,则会创建一个新的实例。如果最后一个创建的实例仍然存活,则返回该实例。仅适用于引用类型
- 模块只能使用组件来创建吗?
不,您可以使用工厂、单例和组件来创建模块,例如:
let module = Module {
component { yourComponent }
factory { YourInstance() }
single { YourSingleton() }
}
- 如何连接委托?
通常,委托模式是循环依赖的主要原因,这些依赖关系位于ViewController或您应用程序中的根组件。您应该使用ModuleBuilder
。这样,您将完全控制如何连接这些委托。
此连接过程是手动的,但以此方式,您将摆脱了解决它们如何连接的问题。
如果您的应用程序使用带有响应式Views
的现代ViewModels
方法,则可能可以删除用于在视图和视图模型之间通信的委托模式。您可以在这里找到一个如何工作的基本示例。
- 您需要两个相同类型/协议的工厂吗?
使用标签
let module = Module {
factory { YourMagicService() as MagicService }
factory(tag: "hisMagicService") { HisMagicService() as MagicService }
}
//then
module.resolve(tag: "hisMagicService")
- 您需要在其他模块中创建新的模块吗?是的,您可以通过简单的父亲模块创建一个新的模块
let module = Module(parent: yourParentModule) {
factory { NewInstance() }
}
请注意,所有工厂都注册到了一个类型/协议,因此,如果您为同一类型编写两个注册项,则最后一个会覆盖第一个,所以请记住这一点。
请尝试有一个金字塔依赖图。
- 您需要从ModuleBuilder之外解决
injectMe
提供的依赖项吗?您可以使用以下方法来完成此操作:
let thing: Magic = resolve()
注意 这只会使用在injectMe
上提供的实例
覆盖和单例实例
工厂只有在提供给模块时才会创建,所以请记住。在共享单例和父模块时,有一个非常重要的注意事项。如果您尝试在父模块中覆盖单例的依赖,而该单例在父模块中已经解析,那么这是不可能发生的,因为这个实例可以创建在您覆盖它之前。让我们通过一个示例来澄清这一点。
let parent = Module {
factory { OneStorage() as Storage }
single { Service(storage: $0()) }
}
let child = Module(parent: parent) {
factory { OtherStorage() as Storage }
}
let service = parent.resolve() as Service
let childService = child.resolve() as Service
子服务存储将与Service相同,因为它已经在父级别解析为单例。但是,如果您更改调用顺序,则单例将与OtherService
存储相关。这可能有点难以理解,但这是一种正常的行为。
在测试时请特别注意这一点,因为如果您在所有测试之间共享同一个模块,这可能会导致这种情况。
为此,正如您将在应用程序测试中看到的,共享模块是一个返回模块的函数,因此我始终获得模块的全新副本,我可以无问题地进行覆盖。
所以,请记住,尽量避免覆盖单例依赖项,因为您认为正在创建单例时,它可能已经创建了。
@Inject
您可以使用 @Inject
属性包装器来自动在您的代码中填充已解析的实例变量。这将使用您resolve实例所在的模块提供的工厂来填充这些变量。
class Bar {}
class Foo {
@Inject var bar: Bar
}
let module = Module {
factory { Foo() }
factory { Bar() }
}
// This will have the bar dependency filled.
let foo = module.resolve() as Foo
您还可以在ModuleBuilder上使用此功能。这些实例将通过在ModuleBuilder中扩展的模块进行解决。
final class MagicModuleBuilder: ModuleBuilder<UIViewController> {
@Inject var some: Some
func component() -> Component? {
Component {
factory { Some() }
}
}
override func build() -> UIViewController {
MagicViewController(some: some)
}
}
注意 在ModuleBuilder的build()
方法内自定义构建的类不会被填充,因为它们并未由模块解决。因此,如果您想要在这些实例内拥有由模块提供的已解决属性,请使用构造函数依赖注入或手动创建后设置它们。
您可以使用@Inject(tag: "yourtag")
来通过@Inject
引用标记依赖。
属性包装器
您可以使用属性包装器来解决由injectMe
通过共享模块提供的先前实例。
@Inject
将在初始化时被实例化,@LazyInject
将在需要时被实例化
@Inject service: Service
@LazyInject service: Service
//With tags
@Inject(tag: "tag") service: Service
@LazyInject(tag: "tag") service: Service
日志记录器
有一个日志记录器将在解决类型时打印所有操作。有三个日志条目
解决类型...
(.debug)
在调用module.resolve()
时记录。
父模块解决类型...
(.debug)
在从父模块获取依赖项时记录。
未找到工厂...
(.error)
在找不到工厂时记录。这将崩溃。
默认情况下,当为DEBUG时,日志级别将是.debug
,否则将是的.error
。您可以通过调用inject(logger: Level)
来更改此默认值。
安装
这是一个预测试版,因此它可能会更改,希望不大,但选项已经在这里。
Swift 包管理器
.Package(url: "https://github.com/JulianAlonso/Injection", majorVersion: 0, minor: 0)
Carthage
将此行添加到您的Cartfile文件中。
github "JulianAlonso/Injection" "master"
Cocoapods
将此行添加到您的Podfile文件中。
pod 'Injection', '~> 0.0'
如果要将徽章添加到您的repo中,请更新README文件,添加以下行。
[](https://github.com/JulianAlonso/Injection)
想要贡献吗?
所有Pull Request都是欢迎的。
对于此项目,在您的本地机器上运行 make bootstrap
开始编码!
测试代码
测试位于 Injection/Test
文件夹中。您可以通过运行 make test
来检查它们是否全部通过。
☕
想要请我喝杯咖啡吗?此选项目前不可用。
更多信息
DSL函数受到 Koin 的启发
作者
开发人员:Julian Alonso,在Twitter上找到我 - @maisterjuli