Deli 是一个易于使用的依赖注入容器,它通过所有必需注册和对应的工厂创建 DI 容器。
目录
概述
想要意大利面吗?还是不?随着项目的发展,将面临复杂。我们可能会不小心写出错误的代码。
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
。为此,请选择 管理方案
并检查 Shared
区域。
或者,您可以用 target
代替 scheme
。在这种情况下,Deli 将找到构建目标。
然后使用提供的二进制文件进行构建。
$ deli build
依赖图通过源代码分析配置。它被保存为你之前指定的文件。
文件内容如下
//
// DeliFactory.swift
// Auto generated code.
//
import Deli
final class DeliFactory: ModuleFactory {
override func load(context: AppContextType) {
...
}
}
将生成的文件添加到项目中,并从应用的启动点调用它。
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
或者,如果您通过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实例成功创建后,可以通过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)》以下列出容器的选择顺序:
- 优先级高者优先。
AppContext.shared.load([
OtherModule.DeliFactory.self,
DeliFactory.self
])
- 当优先级相同时,按照加载顺序。
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. 使用方法
使用上面创建的配置属性有两种方案。
- 修改
deli.yml
。 - 修改构建脚本
按照以下方式修改配置文件
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")
}
InjectProperty
与 ConfigProperty
类似。它在构建时检查配置值,并以字符串类型注入数据。
如果不想要验证就可选地检索配置值,这不是正确的方法。
在这种情况下,建议使用 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以上
致谢
本项目由以下工具支持:
- SourceKitten
- MIT许可协议
- 由JP Simard创建
- Yams
- MIT许可协议
- 由JP Simard创建
- Regex
- Apache License 2.0
- 由Crossroad Labs创建
- Xcproj
- MIT许可协议
- 由xcode.swift创建
- Commandant
- MIT许可协议
- 由Carthage创建
- Quick
- Apache License 2.0
- 由Quick Team创建
- Nimble
- Apache License 2.0
- 由Quick Team创建
许可证
Deli采用MIT协议。请参阅LICENSE文件以获取更多信息。