Injection 0.0.4

Injection 0.0.4

Julian Alonso 维护。



Injection 0.0.4

Injection Logo

Carthage compatible Build Status Tuist Badge

什么是 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文件,添加以下行。

Powered by Injection

[![Powered by Injection](https://img.shields.io/badge/powered%20by-INJECTION-blue.svg?longCache=true&style=flat)](https://github.com/JulianAlonso/Injection)

想要贡献吗?

所有Pull Request都是欢迎的。

对于此项目,在您的本地机器上运行 make bootstrap 开始编码!

🚀

测试代码

测试位于 Injection/Test 文件夹中。您可以通过运行 make test 来检查它们是否全部通过。

想要请我喝杯咖啡吗?

此选项目前不可用。☹️。但我非常感激。

更多信息

DSL函数受到 Koin 的启发

作者

开发人员:Julian Alonso,在Twitter上找到我 - @maisterjuli