依靠注入是一种常见的规划形式,在适宜的时分运用它,能够进步咱们代码的质量。依靠注入是操控回转的一种完成,那么什么是操控回转?
什么是操控回转(Inversion of Control)?
操控回转便是把传统的操控逻辑托付给另一个类或结构来处理,客户方只需完成详细的任务而不需求关怀操控逻辑。
举个例子,比方存在客户方和服务方两个类,客户方需求调用服务方的函数来执行某个逻辑。在传统的编程办法中,客户方依据自己的需求直接去调用服务方的函数从而达到意图。而操控回转,则是把操控逻辑交给服务方,服务方供给了一个操控流的结构,详细的内容需求由客户方来填充,也便是说对流程的操控回转了,现在是服务方调用客户方。听说好莱坞有句名言 Don’t call us, we’ll call you,差不多便是这个意思。以上的服务方也能够是库代码或许结构。
在 iOS 开发中,有一个非常常见的操控回转的完成,或许许多人都没有意识到这个便是操控回转,那便是 completionHandler,或许说 callback。
API.request(completion: { data in
handleData(data)
})
在这个例子中,业务方只需求联系拿到数据以后干什么,而不关怀 completion 的调用时机,把 completion 的调用托付给了网络库,这便是操控回转。
操控回转能够让首要任务和操控逻辑别离,能够提高代码的模块性和扩展性,让代码松耦合,并能够让写测验代码变得简单。
常见的操控回转的完成有:
- 服务定位器(Service Locator)
- 依靠注入
- 上下文查找(Contextualized lookup)
- 模板办法(Template method)
- 策略形式(Strategy design pattern)
本文仅评论依靠注入这种完成,暂不评论其他的完成。
什么是依靠注入?
依靠注入是操控回转的一种详细完成。它在类的外部创立这个类的依靠目标,然后经过某个办法把这个依靠目标供给给类。经过依靠注入,把依靠目标的创立和绑定都移动到了类的外部。
先看下面的例子:
class Car {
var tyres: [Tyre]
init() {
let tyre1 = Tyre()
let tyre2 = Tyre()
let tyre3 = Tyre()
let tyre4 = Tyre()
tyres = [tyre1, tyre2, tyre3, tyre4]
}
}
这个例子中构建了一个汽车目标,汽车目标的构建需求拼装 4 个轮胎。这个代码的缺点显而易见,便是轮胎的创立逻辑和汽车自身耦合了。当咱们想换成另一种轮胎时,或许 Tyre 类调整了完成在构造时增加了一个参数,都必须改动 Car 类中的代码。
用依靠注入的办法,把依靠目标的创立和绑定挪到类外部,就能处理这类问题。
class Car {
var tyres: [Tyre]
init(types: [Tyre]) {
self.types = types
}
}
再举个例子,App 开发中常见的网络恳求 -> 数据处理 -> 数据渲染流程,传统办法开发如下:
// DataViewModel.swift
func loadData() {
API.requestData(id: 2222, completion: { data in
self.handleData(data)
})
}
这样的代码是无法测验的,因为 ViewModel 和详细的网络恳求完成耦合了。为了让 loadData 这个办法能够被测验,应该笼统一个网络恳求的接口,然后从外部注入这个接口的完成。如下代码:
protocol Networking {
func requestData(id: Int, completion: (Data) -> Void)
}
让 DataViewModel 具有一个需求注入的特点目标:
class DataViewModel {
let networking: Networking
init(networking: Networking) {
self.networking = networking
}
}
loadData 办法修改如下:
func loadData(completion: (() -> Void)?) {
networking.requestData(id: 2222, completion: { data in
self.handleData(data)
})
}
这样把详细的网络恳求完成转移到外部注入,在测验的时分就能够简单的完成一个模拟的网络恳求,这样就能够写出测验代码:
func testLoadData() {
let networking = MockNetworking()
let viewModel = DataViewModel(networking: networking)
let expectation = XCTestExpectation()
viewModel.loadData {
expectation.fulfill()
}
wait(for: [expectation], timeout: .infinity)
XCTAssertTrue(viewModel.data.xxxx)
}
依靠注入最大的优点便是完成了类之间的解耦。什么叫解耦,解耦便是两个类之间虽然存在一些依靠联系,可是当其中任何一个类的完成产生改动时,另一个类的完成完全不受影响,解耦自身是经过笼统接口来完成的,因而依靠注入也需求笼统接口,而且依靠注入将依靠的创立转移到了客户类的外部,把依靠的创立逻辑也和客户类自身解耦。这样不管是替换依靠目标的完成类,还是替换依靠目标类完成中的某个部分,客户方类都不需求做任何修改。
依靠注入的品种
依靠注入首要经过初始化器注入、特点注入、办法注入、接口注入等办法来进行注入。
初始化器注入
初始化器注入便是经过初始化办法的参数来给目标供给依靠。初始化器注入是最常用的注入办法,它简单直观,当一个目标依靠的目标的生命周期和目标自身相一起,运用初始化器注入是最好的办法。
class ClientObject {
private var dependencyObject: DependencyObject
init(dependencyObject: DependencyObject) {
self.dependencyObject = dependencyObject
}
}
特点注入
特点注入是经过目标的揭露特点来给目标供给依靠,也能够叫 setter 注入。一般在无法运用初始化器注入时(例如运用了 Storyboard),或许依靠目标的生命周期小于目标时运用。
public class ClientObject {
public var dependencyObject: DependencyObject
}
let client = ClientObject()
client.dependencyObject = DefaultDependencyObject()
办法注入
办法注入的办法是目标需求完成一个接口,这个接口中声明晰能给目标供给依靠的办法。注入器经过调用这个办法来给目标供给依靠。办法注入也能够叫接口注入。
protocol DependencyObjectProvider {
func dependencyObject() -> DependencyObject
}
有时客户方只在某些特定的条件下才需求运用依靠,这时能够用办法注入,客户方仅仅在需求运用依靠的时分才会去调用办法来创立依靠目标,这样避免了客户方在不运用依靠的时分依靠目标也被创立出来占用内存空间的问题。这一点也能够经过注入一个代码块来完成:
init(dependencyBuilder: () -> DependencyObject) {
self.dependencyBuilder = dependencyBuilder
}
依靠注入容器
依靠注入要求目标的依靠在目标外部创立并经过某种办法注入到目标中。如果每次创立目标时都去创立一遍目标的依靠,会让代码变得重复和复杂,当目标的依靠调整时,每个当地都需求做改动。因而一般运用依靠注入时,也需求一个依靠注入容器(Dependency Injection Container)。
依靠注入容器用来统一地办理依靠目标的创立和生命周期,也能够依据需求给目标注入依靠。
依靠注入容器要供给以下的功能:
- 注册(Register):容器需求知道对于一个特定的类型,应该怎样构建它的依靠,容器内会保存类型-依靠的映射信息,并供给接口能够向容器增加这个类型信息,这个操作便是注册。
- 解析(Resolve):当运用依靠注入容器时,就不需求手动创立依靠了,而是让容器帮咱们做这件事情。容器需求供给一个办法来依据类型得到一个目标,容器会创立好这个目标的所有依靠,调用方即可无需关怀依靠,直接运用这个目标即可。
- 处置(Dispose):容器需求办理依靠目标的生命周期,并在依靠目标的生命周期结束时处置它。
完成一个简单的依靠注入容器
有许多第三方依靠注入结构完成了依靠注入的功能,例如 Swift 言语的 Swinject。咱们也能够自己完成一个依靠注入容器。
依照依靠注入容器的界说,并借助 Swift 的泛型和协议,能够界说以下协议:
protocol DIContainer {
func register<Component>(type: Component.Type, component: Any)
func resolve<Component>(type: Component.Type) -> Component?
}
详细完成如下:
final class DefaultDIContainer: DIContainer {
static let shared = DefaultDIContainer()
private init() {}
var components: [String: Any] = [:]
func register<Component>(type: Component.Type, component: Any) {
components["\(type)"] = component
}
func resolve<Component>(type: Component.Type) -> Component? {
return components["\(type)"] as? Component
}
}
有了这个 DIContainer,在运用时能够选择两种办法,一种是在外部 resolve 目标并注入。
let object = DIContaienr.shared.resolve(Data.self)
let viewModel = ViewModel(dependencyObject: object)
一种是在初始化办法的参数默认值中 resolve:
class ViewModel {
init(dependencyObject: DependencyObject = DIContainer.shared.resolve(Data.self))
self.dependencyObject = dependencyObject
}
这两种办法各有一些运用场景,能够依据详细情况选用。
以上的 DIContainer 只是一个简单的完成,结合详细需求,能够增加上线程安全、Storyboard 注入,主动解析等功能,可参考 Swinject。
总结
依靠注入在许多领域都是常见的规划形式,例如 Java Spring 等,不管做哪个方向的开发,依靠注入都是一定要把握的。在适宜的时分运用依靠注入,能够让代码解耦,进步代码的可测验性、可扩展性。
参考资料
- www.tutorialsteacher.com/ioc/ioc-con…
- en.wikipedia.org/wiki/Invers…
- en.wikipedia.org/wiki/Depend…