依赖倒置原则,Dependence Inversion Principle,DIP
罔兩問景曰:「曩子行,今子止,曩子坐,今子起,何其無特操與?」景曰:「吾有待而然者邪!吾所待又有待而然者邪!吾待蛇蚹、蜩翼邪!惡識所以然?惡識所以不然?」
《庄子·齐物论》
软件工程领域,有一个概念叫 SOLID。SOLID 是五个面向对象设计原则的首字母缩写。最早由 Robert C. Martin 在其 2000 年的论文《Design Principles and Design Patterns》中提出的。
其中,D 原则指的是 Dependence Inversion Principle,依赖倒置原则,简称 DIP。
DIP 是这么表述的:
- High level modules should not depend upon low level modules. Both
should depend upon abstractions.
上层模块不应该依赖于底层模块。都应该依赖于抽象。 - Abstractions should not depend upon details. Details should depend upon abstractions.
抽象不应该依赖于细节。细节应该依赖于抽象。
那么,为什么叫依赖倒置呢?
因为传统的架构设计,顶层设计拆分到下层模块实现,下层模块又基于底层模块。像这样:
这个例子中,有个 Copy 任务,它做的事情是把键盘敲入的字符传输到打印机打印出来。那么 C++ 代码实现可能是这个样子:
void Copy () { int c; while ((c = ReadKeyboard()) != EOF) { WritePrinter(c); } }
如果后面,我们又要把键盘敲进的字符写到磁盘里:
这时候,代码可以这么写:
enum class OutputDevice {printer, disk}; void Copy(OutputDevice dev) { int c; while ((c = ReadKeyboard()) != EOF) { if (dev == printer) WritePrinter(c); else WriteDisk(c); } }
如果输入、输出设备种类更多,这样实现的代码就越来越难维护。所以我们需要抽象机制,让依赖关系建立在抽象实体上:
代码实现如下。这时候,Copy 函数就不用关心 Reader 和 Writer 的具体实现,只依赖于抽象类的虚函数接口。
class Reader { public: virtual int Read() = 0; }; class Writer { public: virtual void Write(char) = 0; }; void Copy(Reader& r, Writer& w) { int c; while((c=r.Read()) != EOF) w.Write(c); }
在函数式编程语言中,DIP 原则也很普遍。比如 Clojure 的 map
函数,它不依赖于具体的数据类型,只关心抽象接口。 map
接受实现 clojure.lang.IFn
抽象的函数,处理实现可顺序访问,可 seq
的数据,也就是 seqable?
的数据对象。
例如:
(map - [1 2 3])
(-1 -2 -3)
这里, -
函数实现了 clojure.lang.IFn
:
(instance? clojure.lang.IFn -)
true
而 vec
[1 2 3]
则是 seqable
的:
(seqable? [1 2 3])
true
在 Clojure 中,DIP 原则用的很广泛,从而确保很多核心函数可以充分地在抽象层面重用。