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`。为此,请点击 `Manage Schemes` 并勾选 `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 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
或者,如果你通过 CocoaPods 安装了 Deli,脚本应该如下所示:
"${PODS_ROOT}/DeliBinary/deli" build
类、结构体和协议可以扩展 `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` 类型来加载依赖项实例。
`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
}
}
如果我们能消除所有循环依赖的情况,世界会变得更好,但这不可能完全避免。 解决此问题的一个简单方法是延迟初始化其中一个依赖项。
让我们尝试 `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() {}
}
`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 参数来更改实例行为。)
如前所述,`Autowired` 在 DI 容器中注册。 但是您可能希望在不注册的情况下使用它。 这就是 `Inject`。
class LoginView: Inject {
let viewModel = Inject(LoginViewModel.self)
init() {}
}
class NovelBookView: Inject {
let novels: [Book] = Inject([Book].self, qualifier: "Novel")
init() {}
}
在前端,通常使用用户的数据动态生成模型。 让我们举个例子。
您必须实现一个朋友列表。 当您从朋友列表中选择一个单元格时,您需要显示朋友信息的模式视图。 在这种情况下,朋友数据必须传入 `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 之间的关系延迟注入的。 但是,有效负载通过构造函数注入,因为它由用户传递。
注入依赖项时,需要蓝图。 如上所述,此蓝图是在 `build`(例如 DeliFactory)时生成的。 调用 `AppContext#load()` 时,加载继承了 `ModuleFactory` 的生成类的容器。
Deli 支持多容器。 `ModuleFactory` 可以如下使用。
调用 `AppContext#load()` 时,还会加载模块中的 `ModuleFactory`。
在这种情况下,可以指定 `LoadPriority`。 这是选择用于依赖注入的容器的顺序。
优先级默认为 `normal(500)`。 选择容器的顺序如下:
AppContext.shared.load([
OtherModule.DeliFactory.self,
DeliFactory.self
])
AppContext.shared
.load(DeliFactory())
.load(OtherModule.DeliFactory(), priority: .high)
优先级加载与 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`。
自版本 `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() {}
}
根据运行环境使用不同的配置值通常是有益的。 例如,您可以指定在开发版本中保存文件日志,而在 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
两种解决方案用于使用上面创建的配置属性。
如下更改配置文件
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"
如果配置信息相同,则使用最后指定的信息覆盖它。
您可以使用 `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
}
}
如上所述获取 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"
}
为了增强配置属性的可用性,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")
}
}
为了更容易使用,支持 Swift 5.1 中添加的 @propertyWrapper。
主要支持两个特性:依赖注入和 配置属性。
有 `@Dependency` 和 `@DependencyArray` 用于注入依赖项。
class Library {
@Dependency(qualifier "logger.storage")
var logger: Logger
@DependencyArray(qualifier: "novel")
var novels: [Book]
}
`@PropertyValue` 与 配置属性 相同,用法如下
final class NetworkManager: Inject {
@PropertyValue("server.url")
let serverUrl: String
}
只需将以下行添加到您的 Podfile 中
pod 'Deli', '~> 0.8.1'
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 文件。