控制反转、依赖注入、依赖倒置原则

三个概念傻傻分不清楚,最近结合各种文章学习了下三个概念的异同,在此记录下自己的看法。

这里其实首先需要搞明白两个名词的含义:控制、依赖。

首先是依赖,这个比较简单,调用方使用被调用方的时候,被调用方就是调用方的依赖。比如:


#![allow(unused)]
fn main() {
struct Scrapy;

impl Scrapy {
	fn run() {
		fetch();
		parse();
		download();
	}
}

// 获取 html 文档
fn fetch() {}
// 解析文档获取资源地址
fn parse() {}
// 下载资源
fn download() {}

}

上述代码中,我们实现了一个爬虫 Scrapy,其作为调用方,在 run 函数中调用 fetch, parse, download 函数, 此时就可以说 Scrapy 依赖 fetch, parse, download

然后再来说说什么是控制,控制这个词的含义其实是比较宽泛的,它表明了代码执行的逻辑流程,同时也暗含了依赖关系。 还以上面的代码举例,Scrapy 就有一个控制流程 run,它描述了需要先fetchparse然后download的流程,同时 暗含了Scrapy依赖流程执行过程中所使用的被调用方。

理解了这两个概念后,我们就可以来深入探讨下标题中三个名词的具体含义了。

控制反转

网络上大多数的说法是:控制反转是指框架和程序员(业务代码)之间对流程控制的反转。一般情况下是程序员自己实现流程控制, 引入框架后由框架实现流程控制,程序员只要提供满足框架的依赖即可,即对代码的控制从业务方反转到框架方。

以上述代码为例,如果 Scrapy 是我们的业务代码的一部分,那就是我们作为程序员控制着Scrapy的流程。 但如果Scrapy不是我们业务代码的一部分的话,比如来自一个第三方库,此时Scrapy的控制部分就不受我们掌控,我们需要给Scrapy提供必要的依赖来执行该控制流程。

从辩证的角度来看,我这里其实还有一个相反的理解,即 Scrapy 控制了代码的执行流程,但是没有限制具体依赖。 程序员(业务代码)可以通过传递不同的依赖来控制Scrapy的执行效果。 从这个角度来看,反而是业务方反转着控制了框架方。

从控制执行流程上来看是框架方反转控制业务方;从控制执行细节上来看是业务方反转控制框架方。其实无论是谁反转谁,都有一个必要的前提,那就是框架方的代码需要提供传递依赖的方法,否则任何控制的反转都无从说起。

依赖注入

遗憾的是我们现在的 Scrapy 并没有提供这种传递依赖的方法,它的依赖都是写死的:fetch获取某个 url 的 html 文档,parse 以固定的逻辑解析 html 文档,download 存储在固定的位置上。此时需要 依赖注入 这个具体的编程技巧来完善Scrapy

依赖注入其实很简单,就是不要把依赖写死,由调用方传递依赖进来。但是传递的依赖需要满足特定的要求才行,在常规的面向对象语言中就是满足特定的接口、基类或抽象类等。在 rust 中就是满足 Trait。


#![allow(unused)]
fn main() {
trait Scrapyable {
	fn fetch(&mut self);
	fn parse(&mut self);
	fn download(&mut self);
}

struct Scrapy<T: Scrapyable>(T);

impl<T: Scrapyable> Scrapy<T> {
	fn run(&mut self) {
	    self.0.fetch();
	    self.0.parse();
	    self.0.download();
	}
}
}

我们完善了一下 Scrapy 的实现,现在 Scrapy 已经不依赖具体的方法了,相反的,它设计了一个 Scrapyable Trait(可以类比为 Interface 的概念),只要满足这个 trait 就可以作为依赖传递给它。

struct GoogleScrapy;
impl Scrapyable for GoogleScrapy {
	fn fetch(&mut self) {
		todo!();
	}
	fn parse(&mut self) {
		todo!();
	}
	fn download(&mut self) {
		todo!();
	}
}

struct BaiduScrapy;
impl Scrapyable for BaiduScrapy {
	fn fetch(&mut self) {
		todo!();
	}
	fn parse(&mut self) {
		todo!();
	}
	fn download(&mut self) {
		todo!();
	}
}

fn main() {
	let google_scrapy = Scrapy(GoogleScrapy);
	let baidu_scrapy = Scrapy(BaiduScrapy);
	google_scarpy.run()
	baidu_scarpy.run()
}

作为业务方,我们要实现 Google 和 Baidu 两个不同的爬虫功能,此时只需要让这两个 struct 都实现 Scrapyable trait 即可,具体细节由业务方指定, 只要将其当作依赖传递给 ScarpyScrapy 就可以按照预定的流程执行。

依赖倒置原则

从名字就能看出来这是一个模式设计的指导原则,是从各种实现中提取出来的经验性总结。倒置和反转是同义,这让我们想到控制反转。 控制反转是一种比较笼统的设计思想,前文我们提到控制既表示了流程,也暗含着依赖关系。而依赖倒置原则是指
高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象,是一种更具体的指导编码的设计原则。

我们上面实现的第二版Scrapy其实就遵从了该指导原则,Scrapy 是上层模块,它不依赖底层模块 GoogleScrapyBaiduScrapyScarpyGoogleScrapy 以及 BaiduScrapy 都依赖同一个抽象 Scrapyable

总结

控制反转是设计思想,依赖倒置是指导原则,依赖注入是编码技巧。Scrapy 遵从依赖倒置原则,使用依赖注入技巧,实现了控制反转。