Skip to main content

1.5 依赖倒置原则

定义

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象

补充说明

所谓的高层、低层模块是按照依赖的方向来定的,如模块1依赖模块2,我们就说相对而言,模块1属于高层模块,模块2属于低层模块

“抽象”在代码中具体指的就是接口或者类型,“细节”在代码中具体指的就是模块或者函数

依赖倒置原则具体是指模块或者函数之间不应该直接依赖,而应该是依赖于它们的接口或者类型。所以说,符合依赖倒置原则的编程可以看作是“面向接口/类型编程”

符合依赖倒置原则有下面的好处:

  • 减少细节之间的耦合性
  • 便于替换细节
  • 提高系统的稳定性
  • 提高代码的可读性和可维护性

案例1

以读者读书的系统为例,领域模型如下:

阅读技术书的领域模型图

现在只有技术书,伪代码如下: TechnicalBook

type technicalBook = {
getContent: () => string
}

export let TechnicalBook: technicalBook = {
getContent: () => {
return "技术书的内容"
}
}

Reader

export let read = (technicalBook: technicalBook) => {
let content = technicalBook.getContent()

...
}

Client

Reader.read(TechnicalBook)

现在增加小说书,读者只能阅读技术书或者小说书,这由Client决定。 修改后的领域模型如下:

修改后的领域模型图

修改后的伪代码如下:

TechnicalBook代码不变,故省略

NovelBook

type novelBook = {
getContent: () => string
}

export let NovelBook: novelBook = {
getContent: () => {
return "小说书的内容"
}
}

Reader

export let read = (technicalBook: technicalBook, novelBook: novelBook, needReadBook: string) => {
let content = null

switch (needReadBook) {
case "technical":
content = technicalBook.getContent()
break
case "novel":
default:
content = novelBook.getContent()
break
}

...
}

Client

//这里选择让读者读小说书
Reader.read(TechnicalBook, NovelBook, "novel")

这里的问题是因为Reader依赖了每类书,所以如果一类书发生了变化,或者增加了一类书,那么Reader都需要对应修改代码。

我们需要解除Reader与每类书的依赖,从而使其符合依赖倒置原则。修改后的领域模型如下:

重构后领域模型图

现在提出了书的接口:Book,让Reader改为依赖书的接口,而不依赖书的实现模块。这样做的好处是只要Book接口不变,它的实现模块的变化不会影响到Reader。修改后的伪代码如下: Book

export interface Book {
getContent(): string
}

TechnicalBook

export let TechnicalBook: Book = {
getContent: () => {
return "技术书的内容"
}
}

NovelBook

export let NovelBook: Book = {
getContent: () => {
return "小说书的内容"
}
}

Reader

export let read = (book: Book) => {
let content = book.getContent()

...
}

Client

//这里选择让读者读小说书
Reader.read(NovelBook)