Deli

Swift Version License CI Status Jazzy Platform

Deli 是一个易于使用的依赖注入容器,它可以创建包含所有必需注册信息和相应工厂的 DI 容器。

语言切换:English, 한국어.

目录

概述

想要意大利面?或者不。随着项目的增长,你会经历复杂性。我们可能会错误地编写代码。

Spring framework 中,它提供了使用一些代码规则自动注册,并在运行前抛出错误的依赖关系图。我希望这些功能能在 Swift 中实现。

快速上手

为自动配置文件 `deli.yml` 简化设置。

如果配置文件不存在,则自动查找当前文件夹中唯一项目的构建目标。即使没有指定 `scheme`、`target` 和 `output` 字段,它也能正常工作。

target:
  - MyProject

config:
  MyProject:
    project: MyProject
    scheme: MyScheme
    include:
      - Include files...
    exclude:
      - Exclude files...
    className: DeilFactory
    output: Sources/DeliFactory.swift
    resolve:
      output: Deli.resolved
      generate: true
    dependencies:
      - path: Resolved files...
        imports: UIKit
    accessControl: public

你需要将你的 Scheme 设置为 `Shared`。为此,请点击 `Manage Schemes` 并勾选 `Shared` 区域。

shared-build-scheme

或者,你可以指定 `target` 而不是 `scheme`。在这种情况下,Deli 将查找构建目标。

然后使用提供的二进制文件进行构建。

$ deli build

依赖关系图通过源代码分析进行配置。它会被保存为你之前指定的文件。

文件内容如下

//
//  DeliFactory.swift
//  Auto generated code.
//

import Deli

final class DeliFactory: ModuleFactory {
    override func load(context: AppContextType) {
        ...
    }
}

将生成的文件添加到项目中,并在应用程序的启动点调用它。

drag-and-drop

AppDelegate.swift

import UIKit
import Deli

class AppDelegate {
    
    var window: UIWindow?

    let context = AppContext.load([
        DeliFactory.self
    ])

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        return true
    }
}

构建阶段

将 Deli 集成到 Xcode scheme 中,以在 IDE 中显示警告和错误。只需添加一个新的 "Run Script Phase",内容如下:

if which deli >/dev/null; then
  deli build
else
  echo "error: Deli not installed, download from https://github.com/kawoou/Deli"
fi

Build Phase

或者,如果你通过 CocoaPods 安装了 Deli,脚本应该如下所示:

"${PODS_ROOT}/DeliBinary/deli" build

特性

1. 组件

类、结构体和协议可以扩展 `Component` 协议,并会自动在 DI 容器中注册。

`Component` 可以如下使用:

protocol UserService {
    func login(id: String, password: String) -> User?
    func logout()
}

class UserServiceImpl: UserService, Component {
    func login(id: String, password: String) -> User? {
        ...
    }
    func logout() {
        ...
    }

    init() {}
}

如果编写了以上代码,您可以使用 `UserService` 或 `UserServiceImpl` 类型来加载依赖项实例。

2. 自动注入

`Autowired` 协议会自动注册,与 `Component` 协议相同。不同之处在于,您可以从 DI 容器加载所需的依赖项。

`Autowired` 可以如下使用:

class LoginViewModel: Autowired {
    let userService: UserService

    required init(_ userService: UserService) {
        self.userService = userService 
    }
}

很简单,对吧? 让我们看看下面的代码。

protocol Book {
    var name: String { get }
    var author: String { get }
    var category: String { get }
}

class Novel: Book {
    var qualifier: String {
        return "Novel"
    }

    var name: String {
        return ""
    }
    
    var author: String {
        return ""
    }
    
    var category: String {
        return "Novel"
    }
}

class HarryPotter: Novel, Component {
    override var name: String {
        return "Harry Potter"
    }
    
    override var author: String {
        return "J. K. Rowling"
    }
}

class TroisiemeHumanite: Novel, Component {
    override var name: String {
        return "Troisième humanité"
    }
    
    override var author: String {
        return "Bernard Werber"
    }
}

这段代码通过继承安排书籍。您可以获取所有 `Book` 实例,如下所示:

class LibraryService: Autowired {
    let books: [Book]

    required init(_ books: [Book]) {
        self.books = books
    }
}

此外,应该如何获得带有 "Novel" 限定符的书籍? 在 Deli 中,可以在下面进行构造函数注入:

class LibraryService: Autowired {
    let books: [Book]

    required init(Novel books: [Book]) {
        self.books = books
    }
}

3. 延迟自动注入

如果我们能消除所有循环依赖的情况,世界会变得更好,但这不可能完全避免。 解决此问题的一个简单方法是延迟初始化其中一个依赖项。

让我们尝试 `LazyAutowired` 协议

class UserService: Autowired {
    let messageService: MessageService

    required init(_ messageService: MessageService) {
        self.messageService = messageService
    }
}
class FriendService: Autowired {
    let userService: UserService

    required init(_ userService: UserService) {
        self.userService = userService
    }
}
class MessageService: Autowired {
    let friendService: FriendService

    required init(_ friendService: FriendService) {
        self.friendService = friendService
    }
}

如果您尝试注入 MessageService,将发生循环依赖。

$ deli validate

Error: The circular dependency exists. (MessageService -> FriendService -> UserService -> MessageService)

如果 UserService 扩展了 `LazyAutowired` 会怎样?

class UserService: LazyAutowired {
    let messageService: MessageService!

    func inject(_ messageService: MessageService) {
        self.messageService = messageService
    }

    required init() {}
}

循环被打破,问题得到解决! 在 MessageService 实例成功创建后,可以通过 UserService 所需的 `inject()` 方法注入依赖项。

此外,LazyAutowired 可以像 Autowired 一样指定限定符。 下面的代码注入了一个带有 "facebook" 限定符的 UserService 实例

class FacebookViewModel: LazyAutowired {
    let userService: UserService!

    func inject(facebook userService: UserService) {
        self.userService = userService
    }

    required init() {}
}

4. 配置

`Configuration` 协议使使用者可以直接注册 `Resolver`。

让我们看看代码

class UserConfiguration: Configuration {
    let networkManager = Config(NetworkManager.self, ConfigurationManager.self) { configurationManager in
        let privateKey = "1234QwEr!@#$"
        return configurationManager.make(privateKey: privateKey)
    }

    init() {}
}

您可以看到在创建 NetworkManager 时,privateKey 被传递给 ConfigurationManager。

此 NetworkManager 实例已在 DI 容器中注册,它将作为单例进行管理。 (但是,可以通过更新 scope 参数来更改实例行为。)

5. 注入

如前所述,`Autowired` 在 DI 容器中注册。 但是您可能希望在不注册的情况下使用它。 这就是 `Inject`。

class LoginView: Inject {
    let viewModel = Inject(LoginViewModel.self)

    init() {}
}

class NovelBookView: Inject {
    let novels: [Book] = Inject([Book].self, qualifier: "Novel")

    init() {}
}

6. 工厂

在前端,通常使用用户的数据动态生成模型。 让我们举个例子。

您必须实现一个朋友列表。 当您从朋友列表中选择一个单元格时,您需要显示朋友信息的模式视图。 在这种情况下,朋友数据必须传入 `Info Modal`。

这种情况经常发生,`Factory` 会帮助他们。

让我们尝试 `AutowiredFactory` 协议

class FriendPayload: Payload {
    let userID: String
    let cachedName: String
    
    required init(with argument: (userID: String, cachedName: String)) {
        userID = argument.userID
        cachedName = argument.cachedName
    }
}

class FriendInfoViewModel: AutowiredFactory {
    let accountService: AccountService
    
    let userID: String
    var name: String
    
    required init(_ accountService: AccountService, payload: FriendPayload) {
        self.accountService = accountService
        self.userID = payload.userID
        self.name = payload.cachedName
    }
}

要传递用户参数,您必须实现 `Payload` 协议。 (当然,工厂按原型作用域工作)

实现的 `FriendInfoViewModel` 可以如下使用

class FriendListViewModel: Autowired {
    let friendService: FriendService
    
    func generateInfo(by id: String) -> FriendInfoViewModel? {
        guard let friend = friendService.getFriend(by: id) else { return nil }
        
        return Inject(
            FriendInfoViewModel.self,
            with: (
                userID: friend.id,
                cachedName: friend.name
            )
        )
    }
    
    required init(_ friendService: FriendService) {
        self.friendService = friendService
    }
}

接下来是 `LazyAutowiredFactory` 协议

class FriendInfoViewModel: LazyAutowiredFactory {
    var accountService: AccountService!
    
    func inject(facebook accountService: AccountService) {
        self.accountService = accountService
    }
    
    required init(payload: TestPayload) {
        ...
    }
}

AutowiredFactory 和 LazyAutowiredFactory 之间的区别在于,它是使用 Autowired 和 LazyAutowired 之间的关系延迟注入的。 但是,有效负载通过构造函数注入,因为它由用户传递。

7. 模块工厂

注入依赖项时,需要蓝图。 如上所述,此蓝图是在 `build`(例如 DeliFactory)时生成的。 调用 `AppContext#load()` 时,加载继承了 `ModuleFactory` 的生成类的容器。

Deli 支持多容器。 `ModuleFactory` 可以如下使用。

7.1. 多容器

调用 `AppContext#load()` 时,还会加载模块中的 `ModuleFactory`。

在这种情况下,可以指定 `LoadPriority`。 这是选择用于依赖注入的容器的顺序。

优先级默认为 `normal(500)`。 选择容器的顺序如下:

  1. 优先级高的优先。
AppContext.shared.load([
    OtherModule.DeliFactory.self,
    DeliFactory.self
])
  1. 如果优先级相同,则按加载顺序。
AppContext.shared
    .load(DeliFactory())
    .load(OtherModule.DeliFactory(), priority: .high)

7.2. 单元测试

优先级加载与 7.1 相同,也用于单元测试。

import Quick
import Nimble

@testable import MyApp

class UserTests: QuickSpec {
    override func spec() {
        super.spec()

        let testModule: ModuleFactory!
        testModule.register(UserService.self) { MockUserService() }

        let appContext = AppContext.shared
        beforeEach {
            appContext.load(testModule, priority: .high)
        }
        afterEach {
            appContext.unload(testModule)
        }
        
        ...
    }
}

测试代码的示例是 `Deli.xcodeproj`。

8. 结构体

自版本 `0.7.0` 起,已添加对 Struct 的支持。

基本行为与 Class 相同,但一个区别是无法使用 `weak` 作用域。

下面是 Moya 插件实现的示例。

struct AuthPlugin: PluginType, LazyAutowired {

    var scope: Scope = .weak

    private let authService: AuthService!

    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        var request = request

        if let authToken = authService.authToken {
            request.addValue(authToken.accessToken, forHTTPHeaderField: "Authorization")
            request.addValue(authToken.refreshToken, forHTTPHeaderField: "Refresh-Token")
        }

        return request
    }

    mutating func inject(_ authService: AuthService) {
        self.authService = authService
    }

    init() {}
}

9. 配置属性

根据运行环境使用不同的配置值通常是有益的。 例如,您可以指定在开发版本中保存文件日志,而在 Release 版本中不保存文件日志。

application-dev.yml

logger:
    storage: file

server:
    url: https://dev.example.com/api
    isDebug: false

application-prod.yml

logger:
    storage: default

server:
    url: https://www.example.com/api
    isDebug: true

9.1. 用法

两种解决方案用于使用上面创建的配置属性。

  1. 更改 `deli.yml`。
  2. 修改构建脚本

如下更改配置文件

target:
- MyApp

config:
  MyApp:
    - project: MyApp
    - properties:
      - Configurations/Common/*.yml
      - Configurations/application-dev.yml

构建脚本可以这样做

deli build \
  --property "Configurations/Common/*.yml" \
  --property "Configurations/application-dev.yml"

如果配置信息相同,则使用最后指定的信息覆盖它。

9.2. 组值

您可以使用 `ConfigProperty` 安全地检索配置文件中的指定值。

struct ServerConfig: ConfigProperty {
    let target: String = "server"

    let url: String
    let isDebug: Bool
}

如上所述实现模型时,`ServerConfig` 会在 IoC 容器中注册。

定义模型时要记住的一件事是,需要设置 `target` 值。 此属性表示使用 JSONPath 样式在配置文件中检索的路径。

如果您在构建时没有所需的配置值,将会发生编译错误。

final class NetworkManager: Autowired {
    let info: ServerConfig

    required init(_ config: ServerConfig) {
        info = config
    }
}

9.3. 单个值

如上所述获取 bundle 值时,实现 `ConfigProperty` 协议。 那么如何获得单个值呢? 您可以使用 `InjectProperty`。

final class NetworkManager: Inject {
    let serverUrl = InjectProperty("server.url")
}

`InjectProperty` 类似于 `ConfigProperty`。 它会在构建时检查配置值,并以 String 类型注入数据。

如果您想在没有验证的情况下有选择地检索配置值,这不是正确的方法。

在这种情况下,建议使用 `AppContext#getProperty()` 方法。

final class NetworkManager {
    let serverUrl = AppContext.getProperty("server.url", type: String.self) ?? "https://wtf.example.com"
}

9.4. 通过属性限定符

为了增强配置属性的可用性,Deli 提供了一种使用 `qualifier` 作为配置值进行注入的方法。

有两种使用方法。 让我们首先看看像 `Autowired` 这样的构造函数注入。

Autowired 段落中所述,您不能对指定 `qualifier` 的部分使用 `.`。 不幸的是,swift 没有类似注解的功能。 所以我实现了使用 `comment` 作为替代方案。

它是如何工作的

final class UserService: Autowired {
    required init(_/*logger.storage*/ logger: Logger) {
    }
}

使用 `Inject` 方法时

final class UserService: Inject {
    func getLogger() -> Logger {
        return Inject(Logger.self, qualifierBy: "logger.storage")
    }
}

10. PropertyWrapper

为了更容易使用,支持 Swift 5.1 中添加的 @propertyWrapper

主要支持两个特性:依赖注入和 配置属性

10.1. 依赖

有 `@Dependency` 和 `@DependencyArray` 用于注入依赖项。

class Library {
    @Dependency(qualifier "logger.storage")
    var logger: Logger

    @DependencyArray(qualifier: "novel")
    var novels: [Book]
}

10.2. 属性值

`@PropertyValue` 与 配置属性 相同,用法如下

final class NetworkManager: Inject {
    @PropertyValue("server.url")
    let serverUrl: String
}

安装

Cocoapods:

只需将以下行添加到您的 Podfile 中

pod 'Deli', '~> 0.8.1'

Carthage:

github "kawoou/Deli"

命令行

$ deli help
Available commands:

   build      Build the Dependency Graph.
   generate   Generate the Dependency Graph.
   help       Display general or command-specific help
   upgrade    Upgrade outdated.
   validate   Validate the Dependency Graph.
   version    Display the current version of Deli

示例

贡献

欢迎任何讨论和 pull requests。

如果您想贡献,请提交 pull request

要求

致谢

此项目由以下项目驱动:

许可

Deli 使用 MIT 许可证。 有关更多信息,请参见 LICENSE 文件。