依赖反转原则 Dependency Inversion Principle

在上一篇文章当中我们谈到开放封闭原则,这里我们要来谈谈依赖反转原则 Dependency inversion principle。先不谈定义,先来看范例。

延续上一篇文章的例子,我们有一间专门负责计算面积的公司,方法 calculateArea 定义如下:

const calculateArea = (object) => {
  return object.getArea()
}

由於我们的生意做得太好了,所以有另外一家大公司主动来找我们合作,希望我们加入他们

这家大公司拥有 Tool 类别如下

class Tool {
  calculate: Function

  constructor(calculateFunction: Function) {
    this.calculate = calculateFunction
  }
}

只要在建立实例的时候把 calculateArea 方法给传入,这个 newTool 就成为一个新的、同样能计算面积的工具了!

const a = new Rectangle(13, 17)
const b = new Triangle(11, 19)
const newTool = new Tool(calculateArea)

newTool.calculate(a)   // 221
newTool.calculate(b)   // 104.5

於是乎,Tool 公司开始「依赖」着 calculateArea 部门来服务他的客户。

当依赖变动

不过 calculateArea 这个部门也很有自己的想法。有一天突然想到,除了只回传面积结果之外,如果能够回传多一点的细节,譬如计算面积的总成本,也许会更好。所以 calculateArea 就把回传内容改成下面这样

const calculateArea = (object) => {
  return {
    cost: object.getArea() * 0.01,
    result: object.getArea()
  }
}

结果过没多久,公司就收到一堆人的抱怨,因为原本大家期待呼叫 Tool 的 calculate 方法会得到面积的数值,然而现在却得到了一个物件!

newTool.calculate(a)   // { cost: 2.21, result: 221 }
newTool.calculate(b)   // { cost: 1.045, result: 104.5 }

为了挽救商誉,公司紧急将 Tool 修改成下面这样

class Tool {
  calculateFunction: Function

  constructor(calculateFunction) {
    this.calculateFunction = calculateFunction
  }

  calculate(object) {
    return this.calculateFunction(object).result
  }
}

最後好让使用者得到同样的结果

newTool.calculate(a)   // 221
newTool.calculate(b)   // 104.5

感谢工程师们的努力,公司再次平安度过了一天。然而这家公司还有其他许多的部门,每当这些部门更新或调整各自的方法的时候,Tool 也就会跟着忙着修改,工程师们也就有看起来作不完的事情可以做了。

出现问题

等等,这好像违反了我们前面提到的「单一功能原则」和「开放封闭原则」,为什麽专门负责计算面积的方法更新,Tool 类别也要跟着修改呢?不能只修改一个地方就好了吗?

问题发生的原因是,Tool 的实作依赖着 calculateArea 方法,所以在 calculateArea 方法有修改的情况下,如果 Tool 想要维持同样的产出结果,那麽就必定需要跟着修改。

有没有什麽方法,可以让 Tool 不依赖 calculateArea 方法呢?也就是当 calculateArea 方法变动的时候,Tool 类别自己可以完全不用担心呢?

订定规则

Tool 类别最终还是得靠 calculateArea 方法来计算出面积,所以不可能抛弃他,不过这次公司学乖了,主动跟个别部门并好规则:

「今天不管你各位怎麽计算面积、系统如何更新,我就是要看到数字,其他的我都不想看到」

讲完的同时,公司就提出了一个 AreaCalculator 型别,他定义了方法的输入型别和输出型别,分别是 Shapenumber

type AreaCalculator = (a: Shape) => number;

接着,他继续规定,要传入 Tool 的方法,需要遵守AreaCalculator 型别的规定

class Tool {
  calculate: AreaCalculator

  constructor(calculateFunction: AreaCalculator) {
    this.calculate = calculateFunction
  }
}

这时候 calculateArea 只好摸摸鼻子,遵守了AreaCalculator 型别的规定,规定输入的型别是 Shape 而输出只能是 number

const calculateArea: AreaCalculator = (object: Shape): number => {
  return object.getArea()
}

所以未来不管 calculateArea 如何变动,只要遵守着和 Tool 之间的约定 (AreaCalculator 型别),那麽 Tool 就不需要有任何变动。工程师们突然就失业了!

依赖反转原则

突然之间情势逆转,Tool 类别不再依赖着 calculateArea 方法,这就是「依赖反转」的现象。

所以,究竟什麽是依赖反转原则呢?

In object-oriented design, the dependency inversion principle is a specific form of loosely coupling 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. The principle states:

  • High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

在物件导向程序设计当中,依赖反转原则是一种解耦的形式,根据这个原则执行的时候,高层次的模组 (module) 独立於低层次模组的执行细节。这个原则指出:

  • 高层次的模组不应该依赖於低层次的模组,两者都应该依赖於抽象,譬如介面
  • 抽象不应该依赖於细节,细节应该依赖於抽象

以刚刚的例子为例的话,就是

  • 高层次模组:Tool 类别
  • 低层次模组:calculateArea 方法
  • 被依赖的抽象:AreaCalculator 介面

所以 ToolcalculateArea 两者都依赖 AreaCalculator 介面。从高层次模组的角度来看,他只知道要使用长得像是 AreaCalculator 的东西,但是不需要知道实际上会是什麽东西。从低层次模组的角度来看,必须执行 AreaCalculator 介面,也就是说,当中的执行细节,需要满足这个介面的要求。

小结

「开放封闭原则」让我们能够在不修改(或降低修改)的情况下,持续因应变化扩充功能,而根据「依赖反转原则」,则可以让程序本身不会因为低层次的模组的改变,而需要修正。

回头看刚刚的例子,就是 Tool 能够处理的需求,可以根据传入的 function 进行功能上的扩充,同时透过 AreaCalculator 的设立,让Tool 避免受到低层次模组的影响。


<<:  GoLang 语言

>>:  用React刻自己的投资Dashboard Day13 - 制作分页(Pagination)功能

不只懂 Vue 语法:试说明 Composition API 与 Options API 概念和语法的分别?

问题回答 Composition API 是以逻辑功能来分割程序码,像是写原生 JavaScript...

JavaScript入门 Day01_介绍

因为上一个自我挑战,我耍白痴,打完忘记按发表,所以只能再重新ㄌ呜呜 希望我这次不会再耍白痴了? 嘎油...

【後转前要多久】# Day25 JS - 选取、操作DOM标签

选择HTML标签元素 取得元素 getElementBy document.getElementBy...

[30天 Vue学好学满 DAY5] 生命周期

Vue生命周期(Life Cycle) 每个实例从被初始化,挂载到DOM、更新,到最後被销毁的历程。...

PHP 连接资料库 使用 PDO

PDO PDO 全称 PHP Data Object extension 是 PHP 5.1 开始提...