IoC原则学习

代码耦合的程度一直是用于衡量代码质量的一个重要因素,因此松耦合一直是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 控制反转容器

控制反转容器是一种框架,借助这样的框架,快速实现依赖注入。

四者关系

借用一张图,四者的关系:

Relationships of IoC/DIP/DI/IoC Container

该图从上到下,是从设计原则到实现的结构。唯有 Ioc 和 DIP 结合后,才能实现 DI;而借助 container 框架则可以简化 DI 的实现。
将一些耦合的类修改成松耦合,通常可以通过以下的步骤:

  1. 使用工厂模式,实现 IoC:此时还存在对工厂类和依赖类的耦合
  2. 创建抽象,实现 DIP:此时还存在对工厂类的耦合
  3. 实现 DI:完全去除耦合
  4. 使用 IoC Containcer 来实现 DI

该过程见下图:
loose coupling steps

使用 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 是一个原则,可以使用工厂方法实现,也可以使用其它方法,比如依赖注入。所谓注入,就是在实现依赖关系的时候,给予某个参数,来实现关系调用,可以看下图:

Denpendency Injection Figure

这里有3个概念:

  1. Client Class: 使用服务一方,类似于类 A
  2. Service Class: 真正提供服务的一方,类似于类 B
  3. 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