分享

依赖倒置、控制反转和依赖注入辨析

 汉无为 2022-01-11

依赖与耦合

依赖:依赖描述了两个模型元素之间的关系,如果被依赖的模型元素发生变化就会影响到另一个模型元素
耦合:如果改变程序的一个模块要求另一个模块同时发生变化,就认为这两个模块发生了耦合[Fowler 2001]。

模块间太强的耦合关系给代码的维护带来许多困难,为了解决这个问题,有面向过程和面向对象两种方式的尝试:
面向过程的接口和实现分离
面向对象

依赖倒置(Dependency Inversion Principle)

工厂模式是面对对象编程中常用的设计模式,通过让子类决定该创建的对象是什么,来达到将对象创建过程封装、弱化耦合的目的。这种设计模式利用了依赖倒置的原则。
依赖倒置可以总结为[Martin 1996]:

  • 上层模块不应该依赖于下层模块,它们共同依赖于一个抽象。
  • 抽象不能依赖于具象,具象依赖于抽象。

实现方法:为了消解两个模块间的依赖关系,应该在两个模块之间定义一个抽象接口,上层模块调用抽象接口定义的函数,下层模块实现该接口。
依赖倒置
特点:

  • 应用程序调用类库的抽象接口,依赖于类库的抽象接口;具体的实现类派生自类库的抽象接口,也依赖于类库的抽象接口。
  • 应用程序和具体的类库实现完全独立,相互之间没有直接的依赖关系,只要保持接口类的稳定,应用程序和类库的具体实现都可以独立地发生变化。
  • 类库完全可以独立重用,应用程序可以和任何一个实现了相同抽象接口的类库协同工作。

控制反转(Inversion of Control)

  前面描述的是应用程序和类库之间的依赖关系。如果我们开发的不是类库,而是框架系统,依赖关系就会更强烈一点。那么,该如何消解框架和应用程序之间的依赖关系呢?
  《道法自然》描述了框架和类库之间的区别:
  框架是一个'半成品’的应用程序,而类库只包含一系列可被应用程序调用的类。类库给用户提供了一系列可复用的类,这些类的设计都符合面向对象原则和模式。用户使用时,可以创建这些类的实例,或从这些类中继承出新的派生类,然后调用类中相应的功能。在这一过程中,类库总是被动地响应用户的调用请求。框架则会为某一特定目的实现一个基本的、可执行的架构。框架中已经包含了应用程序从启动到运行的主要流程,流程中那些无法预先确定的步骤留给用户来实现。程序运行时,框架系统自动调用用户实现的功能组件。这时,框架系统的行为是主动的。
  我们可以说,类库是死的,而框架是活的。应用程序通过调用类库来完成特定的功能,而框架则通过调用应用程序来实现整个操作流程。框架是控制倒置原则的完美体现。
  框架系统的一个最好的例子就是图形用户界面(GUI, Graphical User Interface)系统。一个简单的,使用面向过程的设计方法开发的GUI系统如图 5所示。从图 5中可以看出,应用程序调用GUI框架中的CreateWindow()函数来创建窗口,在这里,我们可以说应用程序依赖于GUI框架。但GUI框架并不了解该窗口接收到窗口消息后应该如何处理,这一点只有应用程序最为清楚。因此,当GUI框架需要发送窗口消息时,又必须调用应用程序定义的某个特定的窗口函数(如上图中的MyWindowProc)。这时,GUI框架又必须依赖于应用程序。这是一个典型的双向依赖关系。这种双向依赖关系有一个非常严重的缺陷:由于GUI框架调用了应用程序中的某个特定函数(MyWindowProc), GUI框架根本无法独立存在;换一个新的应用程序,GUI框架多半就要做相应的修改。因此,如何消解框架系统对应用程序的依赖关系是实现框架系统的关键。
  面向过程的GUI框架系统
  模板方法模式是解决框架系统依赖问题的设计模式,这种设计模式利用了好莱坞原则(不要调用我们,让我们调用你)”。GUI框架的一个面向对象的实现如图 7所示。
图7中,“GUI框架抽象接口”是GUI框架系统提供给应用程序使用的接口。抽象出该接口的动机是根据“依赖倒置”的原则,消解从应用程序到GUI框架之间的直接依赖关系,以使得GUI框架实现的变化对应用程序的影响最小化。Window接口类则是“模板方法模式”的核心。应用程序调用CreateWindow()函数时,GUI框架会把该窗口的引用保存在窗口链表中。需要发送窗口消息时,GUI框架就调用窗口对象的SendMessage()函数,该函数是实现在Window类中的非虚成员函数。SendMessage()函数又调用WindowProc()虚函数,这里实际执行的是应用程序MyWindow类中实现的WindowProc()函数。在图 7中,我们已经看不到从GUI框架到应用程序之间的直接依赖关系了。因此,模板方法模式完全实现了回调函数的动态调用机制,消解了从框架到应用程序之间的依赖关系。
这里写图片描述

  总地说来,应用程序和框架系统之间的依赖关系有以下特点:

  • 应用程序和框架系统之间实际上是双向调用,双向依赖的关系。
  • 依赖倒置原则可以减弱应用程序到框架之间的依赖关系。
  • “控制反转”及具体的模板方法模式可以消解框架到应用程序之间的依赖关系,这也是所有框架系统的基础。
  • 框架系统可以独立重用。
      
      对比图 3和图 7可以看出,使用普通类库时,程序的主循环位于应用程序中,而使用框架系统的应用程序不再包括一个主循环,只是实现某些框架定义的接口,框架系统负责实现系统运行的主循环,并在必要的时候通过模板方法模式调用应用程序。也就是说,虽然“依赖倒置”和“控制反转”在设计层面上都是消解模块耦合的有效方法,也都是试图令具体的、易变的模块依赖于抽象的、稳定的模块的基本原则,但二者在使用语境和关注点上存在差异:“依赖倒置”强调的是对于传统的、源于面向过程设计思想的层次概念的“倒置”,而“控制反转”强调的是对程序流程控制权的反转;“依赖倒置”的使用范围更为宽泛,既可用于对程序流程的描述(如流程的主从和层次关系),也可用于描述其他拥有概念层次的设计模型(如服务组件与客户组件、核心模块与外围应用等),而“控制反转”则仅适用于描述流程控制权的场合(如算法流程或业务流程的控制权)。
      从某种意义上说,我们也可以把“控制反转”看作是“依赖倒置”的一个特例。例如,用模板方法模式实现的“控制反转”机制其实就是在框架系统和应用程序之间抽象出了一个描述所有算法步骤原型的接口类,框架系统依赖于该接口类定义并实现程序流程,应用程序依赖于该接口类提供具体算法步骤的实现,应用程序对框架系统的依赖被“倒置”为二者对抽象接口的依赖。

依赖注入(Dependency Injection)

  在前面的例子里,我们通过“依赖倒置”原则,最大限度地减弱了应用程序Copy类和类库提供的服务Read,Write之间的依赖关系。但是,如果需要把Copy()函数也实现在类库中,又会发生什么情况呢?假设在类库中实现一个“服务类”,“服务类”提供Copy()方法供应用程序使用。应用程序使用时,首先创建“服务类”的实例,调用其中的Copy()函数。“服务类”的实例初始化时会创建KeyboardReader 和PrinterWriter类的实例对象。如图 8所示。
  从图 8中可以看出,虽然Reader和Writer接口隔离了“服务类”和具体的Reader和Writer类,使它们之间的耦合降到了最小。但当 “服务类”创建具体的Reader和Writer对象时,“服务类”还是和具体的Reader和Writer对象发生了依赖关系——图 8中用蓝色的虚线描述了这种依赖关系。
  在这种情况下,如何实例化具体的Reader和Writer类,同时又尽量减少服务类对它们的依赖,就是一个非常关键的问题了。如果服务类位于应用程序中,这一依赖关系对我们造成的影响还不算大。但当“服务类”位于需要独立发布的类库中,它的代码就不能随着应用程序的变化而改变了。这也意味着,如果“服务类”过度依赖于具体的Reader和Writer类,用户就无法自行添加新的Reader和Writer 的实现了。

这里写图片描述
  解决这一问题的方法是“依赖注入”,即切断“服务类”到具体的Reader和Writer类之间的依赖关系,而由应用程序来注入这一依赖关系。如图 9所示。
  在图 9中,“服务类”并不负责创建具体的Reader和Writer类的实例对象,而是由应用程序来创建。应用程序创建“服务类”的实例对象时,把具体的Reader和Write对象的引用注入“服务类”内部。这样,“服务类”中的代码就只和抽象接口相关的了。具体实现代码发生变化时,“服务类”不会发生任何变化。添加新的实现时,也只需要改变应用程序的代码,就可以定义并使用新的Reader和Writer类,这种依赖注入方式通常也被称为“构造器注入”。
这里写图片描述

  如果专门为Copy类抽象出一个注入接口,应用程序通过接口注入依赖关系,这种注入方式通常被称为“接口注入”。如果为Copy类提供一个设值函数,应用程序通过调用设值函数来注入依赖关系,这种依赖注入的方法被称为“设值注入”。具体的“接口注入”和“设值注入”请参考[Martin 2004]。
  PicoContainer和Spring轻量级容器框架都提供了相应的机制来帮助用户实现各种不同的“依赖注入”。并且,通过不同的方式,他们也都支持在XML文件中定义依赖关系,然后由应用程序调用框架来注入依赖关系,当依赖关系需要发生变化时,只要修改相应的 XML文件即可。
  因此,依赖注入的核心思想是:
  1. 抽象接口隔离了使用者和实现之间的依赖关系,但创建具体实现类的实例对象仍会造成对于具体实现的依赖。
  2. 采用依赖注入可以消除这种创建依赖性。使用依赖注入后,某些类完全是基于抽象接口编写而成的,这可以最大限度地适应需求的变化。

结论

  分离接口和实现是人们有效地控制依赖关系的最初尝试,而纯粹的抽象接口更好地隔离了相互依赖的两个模块,“依赖倒置”和 “控制反转”原则从不同的角度描述了利用抽象接口消解耦合的动机,GoF的设计模式正是这一动机的完美体现。具体类的创建过程是另一种常见的依赖关系,“依赖注入”模式可以把具体类的创建过程集中到合适的位置,这一动机和GoF的创建型模式有相似之处。
  这些原则对我们的实践有很好的指导作用,但它们不是圣经,在不同的场合可能会有不同的变化,我们应该在开发过程中根据需求变化的可能性灵活运用。


本文改编自http://dotnetfresh.cnblogs.com/archive/2005/06/27/181878.html

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多