依赖反转原则与ioc/di的关系

依赖反转原则 - Dependency Inversion Principle

wiki上对依赖反转原则说明如下:

In object-oriented design, the dependency inversion principle refers to a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details.

该原则规定:

  1. 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
  2. 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

通常的抽象都来源于细节,来源于具体对象,通过对这些具体对象进行分析思考,从而形成高层的抽象对象或者是接口,所以直观的来看一般都是抽象依赖于细节,而DIP原则正好是将之倒置,要求细节依赖于抽象。

上图高层模块1中对象A依赖于具体对象B,图中模块1重构后,对象A依赖于低层模块中对象B的抽象接口InferfaceB,而在低层模块2中的对象B实现了此接口,一般通过IoC/DI完成对象调用,从而达到细节依赖抽象,而不是抽象依赖细节,这就是依赖反转。

面向对象编程中,最根本的一个原则就是分离变化与不变化的部分,面向对象的三大特性(封装、继承和多态),六大原则(SOLID),若干设计模式,本质都是体现这个原则,即分离对象中变化和不变化的部分,分离模块中变化与不变化的部分,依赖反转原则中抽象的部分就是相对不变的部分,变化的部分即具体的细节的低层对象。

控制反转 - Inversion of Control

wiki上对控制反转的说明如下:

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的实现方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫依赖查找(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

in software engineering, inversion of control (IoC) is a design principle in which custom-written portions of a computer program receive the flow of control from a generic framework. a software architecture with this design inverts control as compared to traditional procedural programming: in traditional programming, the custom code that expresses the purpose of the program calls into reusable libraries to take care of generic tasks, but with inversion of control, it is the framework that calls into the custom, or task-specific, code.

inversion of control is used to increase modularity of the program and make it extensible, and has applications in object-oriented programming and other programming paradigms. the term was popularized by Robert C. Martin and Martin Fowler.

the term is related to, but different from, the dependency inversion principle, which concerns itself with decoupling dependencies between high-level and low-level layers through shared abstractions. the general concept is also related to event-driven programming in that it is often implemented using ioc, so that the custom code is commonly only concerned with handling of events, whereas the event loop and dispatch of events/messages is handled by the framework or the runtime environment.

依赖注入 - Dependency Injection

一般说到控制反转,都会和依赖注入一起提到,从上面的定义也可以知道,IoC是一个设计原则,而依赖注入是它的实现方式之一,依赖注入这个概念的提出可以参考 Martin Fowler 的这篇文章,对应的中文版本

关于IoC/DI的的一些实现方式可以参考上面 Martin Fowler 的文章,接下来说一下DIP和IoC这2个设计原则的关系。

DIP是更抽象的编程原则,在java编程中,用一句话来表示就是面向接口编程,具体表现如下:

  1. 不同的模块之间通过接口发生依赖。
  2. 接口或者抽象类依赖于其他的接口或者抽象类,而不依赖于具体的实现类。
  3. 低层次的具体实现类则依赖于接口或者抽象类。

IoC中的反转是针对谁来控制对象的创建而言,由原来调用者直接创建对象反转为由IoC容器创建对象并注入到调用者中,进而实现代码之间的松耦合,IoC中对象之间的依赖可能是依赖于低层次的具体实现类,也可能是依赖于接口或者抽象类。如果低层次的对象是依赖于接口的,而依赖注入的就是这个接口的具体实现类,那么这就是依赖反转的直接体现了。

上图中原来对象A直接依赖于对象B,直接反映到代码里就是在对象A中直接new出对象B,这样对象A和对象B就紧耦合了,为了将二者达到松耦合,引入了一个中间对象,这就是IoC容器,通过IoC容器创建对象B,再将对象B注入到对象A中,这样对象A就避免了创建对象B。

这时IoC/DI的任务其实已经完成了,容器负责创建对象并完成依赖注入,代码在谁创建对象这件事情上做了解耦,但是如果对象A是依赖于具体细节实现的对象B,那么设计仍然没有遵循依赖反转原则,高层的模块中,对象A直接依赖于低层的具体对象B,即依赖于细节,细节发生变化,那么高层模块必须也需要跟着相应发生变化。

这也说明了依赖反转原则DIP是更高级的一个抽象原则,一般通过IoC/DI实现,如果对象A依赖于抽象,比如是依赖于接口,而对象B实现此接口,那也就达到了依赖反转的效果,如果发生变化,只要通过IoC/DI注入接口的另一个实现即可,高层模块的代码就不用变化。

通过上面的说明,可以看出依赖反转后,分离变化与不变化的部分,低层模块中的对象B发生变化时,高层模块相对来说是不变的,并不需要调整代码。

解耦与分层

java开发中大部分设计模式本质上就是为了对象之间的解耦,通过在原本紧耦合的对象中间加入一个中间对象,以达到解耦的目的。

为什么需要解耦,就是为了分离变化与不变化的部分,如果代码不会变化,代码内部写得耦合度高,也是可以的,只要逻辑清楚,代码可读性维护性好就可以,解耦也只是为了提高代码稳定性和模块的重用,不要为了解耦而解耦,很多模式为了解耦,也是引入了一定的复杂性的。

现在的web项目中MVC模式是用得最多的,原来在view层直接注入Model层的话,代码耦合度太高,View和Model各自都绑定对方,很难代码重用,加入了中间Controller一层作解耦之后,代码和逻辑都更加清晰,并且职责更加明确,Model重用性也更好,而View中很多组件中变化的部分都被分离出去,从而达到非常高的复用性,但作为中间胶水的Controller则一般难以重用。

在简单工厂中,为避免暴露对象创建的细节给客户端,并且客户端根据DIP原则,只要依赖所需对象的接口即可,对象的创建由作为中间层的工厂来创建,简单工厂其实很接近IoC容器的实现了。

中介者模式就更加明显,在二个原来相互依赖的对象之间,加入一个中介者(Mediator)进行解耦。

适配器在Target和Adaptee之间加入了一个中间对象Adapter,通过这个Adapter将目标接口的请求委托到原来存在的Adaptee对象上。

代理模式中,在real对象和客户端之间加入了一个Proxy对象,由这个代理对象控制客户端对真实对象的访问,代理模式中改变的是被代理对象的行为,而不改变接口,而适配器模式则刚好相反,改变的是接口,而不改变被适配对象的行为。

门面模式也是在细碎复杂的接口和客户端之间加入一个Facade对象,为复杂的接口提供一个常用的简单接口。

命令模式中,为了避免请求Request直接发给接收者Receiver,在中间加入了一个命令Command的角色,将具体的接收者绑定到某个具体命令中,请求只是简单的调用命令的接口即可,不需要知道接收者的情况,从而达到二者之间的解耦。

观察者模式中,在subject对象(JDK实现为Observable)和众多观察者之间加入了中间对象(JDK实现中的Vector),通过这个中间对象解耦Subject和Observers,并且管理所有观察者,Subject发生变化时,由这个中间对象通知所有观察者。

访问者模式中,因为对象行为是变化不确定的,为解耦对象的内部状态和行为,引入了一个访问者Visitor,每个访问者定义了一个行为或者操作,将这个Visitor传到对象中再执行这个操作。

迭代器模式中,则是在数组或者集合前加入一个迭代器Iterator,客户端只要操作这个迭代器就可以遍历集合内的所有元素,而不用关注具体是什么集合,以及如何遍历这个集合。

状态模式中,原来将状态和行为都耦合在Context对象中,通过if/else进行状态判断后执行相应的行为,行为对状态决定,通过加入一个State接口,按照状态创建对象,并将状态对应的行为封装在状态对象内部,并在状态对象内部控制Context对象的状态变化,从而达到状态与行为的高内聚,状态和Context对象的松耦合。

生成器模式中加入了Builder,由Builder创建产品的不同部分,再由Director负责产品整体组装,多数情况下客户端调用者直接扮演了Director的角色,也就是在具体产品和客户端中加入了一个中间对象Builder。

享元模式本质上就是使用共享技术有效地支持大量细粒度对象的复用,为了达到复用,其核心思想就是分离变化与不变化,将不变的部分作为享元对象的内部状态,将变化的部分状态分离出去,从而避免生成大量的对象,以参数的形式传入给享元对象,一般享元模式中会搭配一个简单工厂用来创建和管理享元对象,所有生成的享元对象会缓存在这个工厂对象中,外部通过工厂获取享元对象,因此这个享元对象的管理工厂可以认为是一个中间对象。JDK中的String常量池和Integer的-128127这些常用对象的池化也是为了对象复用,节省内存而设计的。

模板方法和工厂方法主要是分离对象中变化和不变化部分,相对不变的部分放在抽象基类中,变化部分由子类去实现,所以这2个模式目的不是为解耦对象关联,也没有引入中间对象,这里的工厂方法(Factory Method)里生产的对象一般来说是工厂内部使用的,如果生成的对象是给客户端调用的,那这个工厂就是一个中间对象。

References

  1. 依赖反转原则
  2. Inversion of control
  3. Dependency injection
  4. 控制反转
  5. 控制反转与依赖注入