代码耦合的程度一直是用于衡量代码质量的一个重要因素,因此松耦合一直是coder孜孜不倦追求的目标。如何降低耦合性,从理论到实践都有多种方法,今天,来学习 IoC 控制反转。
IoC 理论
想必大家都听到过诸如IoC,DI的名词,它们究竟代表什么,又各自具有什么关系呢?
- IoC: Inversion of Control, 控制反转
- DIP: Denpendency Inversion Principle, 依赖反转原则
- DI: Dependency Inject, 依赖注入
- IoC container: 控制反转容器,用于实现DI的框架
IoC 控制反转
控制反转是一种设计原则。所谓控制,在 OO(object oriented) 的世界中,指的是类除了承担其本身的单一指责外,具有的责任,如:应用的执行顺序(控制流),依赖对象的创建和绑定等等。控制反转的目的就是为了实现解耦(松耦合)。
DIP 依赖反转原则
依赖反转原则也是一种设计原则,用于实现依赖方之间的松耦合,其核心主要是:高层模块不应依赖底层模块;高层和底层模块都应该依赖于抽象。
DI 依赖注入
依赖注入是一种设计模式,是对IoC的实现。
IoC container 控制反转容器
控制反转容器是一种框架,借助这样的框架,快速实现依赖注入。
四者关系
借用一张图,四者的关系:
该图从上到下,是从设计原则到实现的结构。唯有 Ioc 和 DIP 结合后,才能实现 DI;而借助 container 框架则可以简化 DI 的实现。
将一些耦合的类修改成松耦合,通常可以通过以下的步骤:
- 使用工厂模式,实现 IoC:此时还存在对工厂类和依赖类的耦合
- 创建抽象,实现 DIP:此时还存在对工厂类的耦合
- 实现 DI:完全去除耦合
- 使用 IoC Containcer 来实现 DI
该过程见下图:
使用 IoC 解藕
下面,通过实例来学习相关概念和如何从理论到实践实现 IoC。
依赖
在此之前,想先探讨下什么是代码的耦合。在 OO 的设计理念中,我们无可避免的和类打交道,当某个类A需要使用到类B的方法时,我们可以这样简单地实现:
Class A {
B bInstance;
A (){
b = new B();
}
task(){
b.method()
}
}
Class B {
method(){
//
}
}
当类A中持有一个类B的实例,需要在A中实例化B然后调用B的方法,这种情况就是强耦合。这种“强”提醒在: * 当 A 需要使用其它类C来实现B的功能时,需要修改实例的声明,创建和调用 * 当 B 更新了相关接口,A 也要做适配
我们需要分清楚的是类A中使用类B不是代码的耦合,因为这种调用或者说依赖关系往往是由业务决定的。我们关注的强耦合指的是当依赖关系中的某一方有了“变化”后,需要在另一方做“适配”,否则不能正常工作。
使用IoC
我们可以使用 IoC 原则,将这样的依赖关系反转。以上述的代码为例,就是将在 A 中创建 B 实例这个依赖消除。使用工厂设计模式,可以实现, 如:
Class A {
task(){
B bInstance = Factory.createB()
bInstance.method()
}
}
这样,A 与 B 之间的依赖关系,转移到了工程类。但是,很显然,A 除了需要声明 B,并且对工厂类的依赖也是一种强耦合。使用 IoC 用于无法消除此类耦合。于是,我们需要使用 DIP。
使用DIP
DIP 的定义可由以下原则体现:
1. 高层模块不应该依赖于低层模块,而应该依赖抽象
2. 抽象不应该依赖于具体实现,而是具体实现依赖于抽象
因为A 调用 B,所以 A 相对于 B 是高层模块或者说 A 依赖于 B。接下来,我们需要让 A 和 B 都依赖于“抽象”。抽象和封装是OO中的场景术语,所谓抽象,和具象相对,它的一个必要条件就是不能被实例化。对应到代码中,往往代表着抽象类或者是协议/接口。而要“正确”地定义抽象,需要从业务出发,理解相关联的业务方之间是如何发生关系的。对应到代码,你需要正确地理解相依赖的两个模块关系:A 使用 B 进行了何种操作。因此,通常情况下,抽象就是定义了一组行为或接口,各个具体的类按各自实际去实现行为或接口。
protocol CallTask{
commonTask();
}
Class B: CallTask {
commonTask() {
//
}
}
Class Factory {
CallTask createTasker() {
return new B();
}
}
Class A {
method(){
CallTask caller = Factory.createTasker();
caller.commonTask();
}
}
使用 DIP 首先需要定义抽象——这里定义了一个协议,B 需要实现该协议,而工厂方法生成的实例返回的是实现了协议的对象。最后,在 A 中已经看不到 B,实现了依赖于抽象。
依赖注入
前面说过, IoC 是一个原则,可以使用工厂方法实现,也可以使用其它方法,比如依赖注入。所谓注入,就是在实现依赖关系的时候,给予某个参数,来实现关系调用,可以看下图:
这里有3个概念:
- Client Class: 使用服务一方,类似于类 A
- Service Class: 真正提供服务的一方,类似于类 B
- Injector Class: 注入类,将提供服务的对象提供给客户方使用
简单地说,当 A 需要使用 B 做某事时,将创建 B 的行为以及调用行为通过注入的方式实现。
三种类型
- Constructor 注入:通过构造器初始化的时候注入依赖项
- Property 注入:将依赖性作为属性注入
- Method 注入:将依赖项作为方法注入
以构造器注入为例:
protocol CallTask{
commonTask();
}
Class B: CallTask {
commonTask() {
//
}
}
Class A {
CallTask callee;
initWith(call: CallTask){
callee = call;
}
method(){
callee.commonTask();
}
}
属性和方法注入也是类似的,区别在于不同的时机将提供服务一方注入到调用方。
IoC 容器
IoC 容器就是一个用于实现 IoC 的框架,其目的在于简化 IoC 的实现,将我们的注意力关注在服务方和客户方,同时也管理着对象的生命周期。
所有的 IoC 容器实现都包含3个部分:
- Register: 容器需要知道类型和依赖关系。
- Resolve: 创建对象的工作交由容器来处理;根据 Register 内的信息,创建对象并注入依赖关系
- Dispose: 管理对象的生命周期,以及移除不需要的类型和依赖
Swinject
Swinject
是我用到的一个使用 Swift 实现依赖注入的框架。之所以使用该框架是因为在2018年初发现某个老项目中使用到的 API 下线了,需要修改。当时修改的时候就在想:如果 API 以后又有了变化该怎么办?然后就发现了 Swinject
,恰好其也提供了一个入门级的使用教程。简单的说,项目业务包含:
UI --- Network --- DataHandle
iOS 应用中 UIViewController
除了负责构建 UI 外,还会负责获取数据。
class ZZHCurViewController: UIViewController {
var requestService: ZZHCurRequestService?
}
class ZZHCurRequestService: NSObject {
static func getRequest(){
}
static func decode(data: Data) ->[Dictionary<String: Any>] {
}
}
如果这样做,那么 ZZHCurViewController
持有 ZZHCurRequestService
就意味着强耦合。当遇到 Service
有变化的时候,其依赖方都要收到影响。一个很好的解耦方式就是定义一组协议,将各个模块之间的关系抽象出来。具体的操作过程,请看前文提到的教程,这里不做搬运了。
参考
TutorialsTeacher 的文章通俗易懂;看中文的话,可以看来自 cnblogs 的文章。需要侧重实践的,可以看 Swinject 内提供的 blog,无论使用 MVC 亦或 MVVM 都有示例可查。
Inversion of Control by TutorialsTeacher
深入理解DIP、IoC、DI以及IoC容器 by cnblogs
Swinject
Comments