Deli 0.9.0

Deli 0.9.0

Kawoou 维护。



Deli 0.9.0

Deli

Swift Version License CI Status Jazzy Platform

Deli 是一个易于使用的依赖注入容器,它通过所有必需注册和对应的工厂创建 DI 容器。

语言切换:英文한국어

目录

概述

想要意大利面吗?还是不?随着项目的发展,将面临复杂。我们可能会不小心写出错误的代码。

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

入门

自动配置文件 deli.yml 的简单设置。

如果配置文件不存在,它会自动在当前文件夹中找到一个独特项目的构建目标。即使在没有任何 schemetargetoutput 字段的情况下,也会以相同的方式工作。

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。为此,请选择 管理方案 并检查 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方案中,以便在IDE中显示警告和错误。只需添加一个新的“运行脚本阶段”

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() {}
}

如果写下上述代码,您可以使用UserServiceUserServiceImpl类型来加载依赖实例。

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实例成功创建后,可以通过inject()注入UserService所需依赖。

此外,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容器中,它将被当作单例管理。(然而,可以通过更新作用域参数来改变实例的行为。)

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的关系惰性注入。但是,由于用户传递,由构造函数注入payload。

7. 模块工厂

在注入依赖时,需要蓝图。如上所述,该蓝图在《构建》(例如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》。

其基本行为与类相同,但有一点不同,那就是不能使用《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. 配置属性

根据运行环境使用不同的配置值很常见。例如,您可以在开发构建中指定保存文件日志,而在发布构建中则不保存文件日志。

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. 单值

当获取如上所示的捆绑值时,实现 ConfigProperty 协议。那么如何获取单个值呢?您可以使用 InjectProperty

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

InjectPropertyConfigProperty 类似。它在构建时检查配置值,并以字符串类型注入数据。

如果不想要验证就可选地检索配置值,这不是正确的方法。

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

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

9.4. 通过属性指定限定符

为了提高配置属性的可用性,Deli 提供了一种使用配置值作为限定符进行注入的方法。

有两种使用它的方法。我们首先看构造函数注入,类似于 Autowired

Autowired段中所述,你无法为指定限定符的部件使用 .。不幸的是,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. 属性包装器

为了便于使用,支持了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

示例

  • DeliTodo:使用Deli的iOS Todo应用程序。
  • GitHubSearch:使用Deli实现的GitHub搜索示例。
  • Survey:使用Deli实现的调查示例。
  • RobotFactory:使用Deli实现的机器人工厂示例。

贡献

欢迎任何讨论和拉取请求。

如果您想贡献,请提交拉取请求

要求

  • Swift 3.1以上

致谢

本项目由以下工具支持:

许可证

Deli采用MIT协议。请参阅LICENSE文件以获取更多信息。