Open chunpu opened 1 year ago
这几个原则和设计模式核心目的都是为了解耦,降低复杂度。
之所以把这三个概念放在一块讲是因为生活中很多例子都用到了里面的思想。
传统模式下我们出行需求是自己买一个车,自己去店里买车,买车位,买车险,车坏了就自己修,不想要的时候还要自己卖车,虽然控制权完全在自己手中,但需要操心的事很多。
强控制下意味着强耦合,麻烦事也多,那如何解耦呢?
解法就是放弃控制权,变成松耦合。
我们选择在需要用车的时候打车租车,这样我们不用买车,租车位,买保险,管这些杂事。
看一下我们的解耦成果,我们依赖的是滴滴这样的打车租车软件,而不是一辆具体的车。这样我们不再关心车的各种杂事,也非常容易换车。
依赖注入是一种设计模式,也是控制反转的一种具体实现。
注入的字面意思是把外部的东西放到内部,与之相反的就是内部的东西由内部自然产生。 依赖注入的具体实现是传参,最常见的就是构造函数的参数。
const dataService = new DataService() const myModule = new MyModule(dataService)
MyModule 没有用自己去创建 dataService,而是通过构造函数的参数直接拿了外部的 dataService。 dataService 被注入到了 myModule 中。
依赖注入解耦了什么?
传统模式:高层模块使用低层模块,高层模块必须 import 低层模块。 依赖注入模式:高层通过接口参数的方法使用低层模块的实例,不再有直接的 import 关系。 注意,即便是使用实例,我们也要知道这个实例的类型,因此高层还是需要依赖一个接口, 而此处用到了依赖倒置。
依赖倒置更合适的名字应该叫面向接口编程
注意,依赖倒置是一种面向对象的设计原则,而非设计模式。
依赖倒置原则的核心思想是:
依赖倒置原则下多了一个接口中间层:
为什么叫倒置呢?因为传统编程是高层模块依赖低层模块,而依赖倒置则认为低层模块要依赖接口中间层定义。
比如一个文件管理 App 要依赖文件 fs 模块。
一开始我们只需要 windows 平台,所以支持了 windows 的 fs,现在客户希望 mac 也支持,就无法轻易实现,因为我们无法轻易替换低层模块。但如果客户端程序设计之初就是依赖一个 fs 接口,而各端的 fs 模块又实现了这个 fs 接口,那 fs 模块就可以随意替换了。
生活中的例子就是 HDMI 接口,如果电脑和显示器在设计的时候是直接连接的,那电脑和显示器就无法分离,完全耦合。但电脑和显示器是分体式设计,通过 HDMI 接口来连接,这样显示器可以任意换主机,主机也可以任意换显示器。
我们发现,不仅是高层低层模块之间的耦合,其实任何大型模块的耦合,如果未来有替换的可能,都应该通过接口来耦合,这样我们的模块就解耦了,可以单独用于测试,将来重构也很容易。
在实际开发中,我们应该先定义接口,再进行模块开发。高层模块依赖并调用接口,低层模块依赖并实现接口。
// 定义一个接口,表示依赖的功能 interface ILogger { log(message: string): void } // 实现一个具体的依赖 class ConsoleLogger implements ILogger { log(message: string): void { console.log(message) } } // 使用依赖的类 class UserService { private logger: ILogger // 通过构造函数进行依赖注入 constructor(logger: ILogger) { this.logger = logger } addUser(name: string): void { this.logger.log(`Adding user: ${name}`) // 其他逻辑... } } // 创建依赖实例 const logger = new ConsoleLogger() // 创建使用依赖的实例,并通过依赖注入传入依赖实例 const userService = new UserService(logger) // 使用依赖的功能 userService.addUser('John Doe')
传统模式下对象自己控制自己,反转成 IOC 容器来控制对象。
IOC 容器非常有误导性,容器的特点是独立,隔离等,所以 docker 容器非常好理解。 而 IOC 容器更好的名字应该叫:实例依赖管理器。
传统模块下,模块控制自己的依赖,比如依赖的实例化等。 控制反转设计模式下,由实例依赖管理器(IOC 容器)控制各个模块。
interface IocContainer { // 注册一个功能类,dependency 一般是一个类 register<T>(name: string, dependency: T): void // resolve = 获得单例,初始化,resolve 可以是 async resolve<T>(name: string): T }
一个典型的 IOC 容器例子如下
class LazyInitializedObject { constructor(param) { this.param = param console.log(`LazyInitializedObject is being created with param: ${param}`) } async initialize() { // 模拟异步请求 return new Promise((resolve) => { setTimeout(() => { console.log( `LazyInitializedObject is initialized with param: ${this.param}` ) resolve() }, 2000) }) } doSomething() { console.log('LazyInitializedObject is doing something.') } } class Container { constructor() { this.dependencies = {} } register(name, dependency) { this.dependencies[name] = dependency } async resolve(name, ...args) { const dependency = this.dependencies[name] if (dependency) { if (typeof dependency === 'function') { // 如果依赖是一个构造函数,则延迟初始化并存储实例 const instance = new dependency(...args) await instance.initialize() // 异步初始化 this.dependencies[name] = instance // 缓存实例 return instance } else { // 如果依赖已经是一个实例,则直接返回 return dependency } } throw new Error(`Dependency not found: ${name}`) } }
IOC 容器是可以嵌套的,就像一个组件树一样,IOC 容器 的 dependencies 可以是一个 IOC 容器,然后它又有自己的 dependencies。
我写了一个非常简单的 ioc 容器 https://github.com/chunpu/minioc/blob/master/example/example.ts, 越简单越容易理解哦~
依赖倒置,依赖注入,控制反转都是为了解耦。
名词解释
这几个原则和设计模式核心目的都是为了解耦,降低复杂度。
之所以把这三个概念放在一块讲是因为生活中很多例子都用到了里面的思想。
生活场景类比
传统模式下我们出行需求是自己买一个车,自己去店里买车,买车位,买车险,车坏了就自己修,不想要的时候还要自己卖车,虽然控制权完全在自己手中,但需要操心的事很多。
强控制下意味着强耦合,麻烦事也多,那如何解耦呢?
解法就是放弃控制权,变成松耦合。
我们选择在需要用车的时候打车租车,这样我们不用买车,租车位,买保险,管这些杂事。
看一下我们的解耦成果,我们依赖的是滴滴这样的打车租车软件,而不是一辆具体的车。这样我们不再关心车的各种杂事,也非常容易换车。
如何理解依赖注入(DI)
依赖注入是一种设计模式,也是控制反转的一种具体实现。
注入的字面意思是把外部的东西放到内部,与之相反的就是内部的东西由内部自然产生。 依赖注入的具体实现是传参,最常见的就是构造函数的参数。
MyModule 没有用自己去创建 dataService,而是通过构造函数的参数直接拿了外部的 dataService。 dataService 被注入到了 myModule 中。
依赖注入解耦了什么?
传统模式:高层模块使用低层模块,高层模块必须 import 低层模块。 依赖注入模式:高层通过接口参数的方法使用低层模块的实例,不再有直接的 import 关系。 注意,即便是使用实例,我们也要知道这个实例的类型,因此高层还是需要依赖一个接口, 而此处用到了依赖倒置。
依赖倒置
依赖倒置更合适的名字应该叫面向接口编程
注意,依赖倒置是一种面向对象的设计原则,而非设计模式。
依赖倒置原则的核心思想是:
依赖倒置原则下多了一个接口中间层:
为什么叫倒置呢?因为传统编程是高层模块依赖低层模块,而依赖倒置则认为低层模块要依赖接口中间层定义。
比如一个文件管理 App 要依赖文件 fs 模块。
一开始我们只需要 windows 平台,所以支持了 windows 的 fs,现在客户希望 mac 也支持,就无法轻易实现,因为我们无法轻易替换低层模块。但如果客户端程序设计之初就是依赖一个 fs 接口,而各端的 fs 模块又实现了这个 fs 接口,那 fs 模块就可以随意替换了。
生活中的例子就是 HDMI 接口,如果电脑和显示器在设计的时候是直接连接的,那电脑和显示器就无法分离,完全耦合。但电脑和显示器是分体式设计,通过 HDMI 接口来连接,这样显示器可以任意换主机,主机也可以任意换显示器。
我们发现,不仅是高层低层模块之间的耦合,其实任何大型模块的耦合,如果未来有替换的可能,都应该通过接口来耦合,这样我们的模块就解耦了,可以单独用于测试,将来重构也很容易。
在实际开发中,我们应该先定义接口,再进行模块开发。高层模块依赖并调用接口,低层模块依赖并实现接口。
控制反转
传统模式下对象自己控制自己,反转成 IOC 容器来控制对象。
IOC 容器(IOC Container,Dependency Injection Container)
IOC 容器非常有误导性,容器的特点是独立,隔离等,所以 docker 容器非常好理解。 而 IOC 容器更好的名字应该叫:实例依赖管理器。
传统模块下,模块控制自己的依赖,比如依赖的实例化等。 控制反转设计模式下,由实例依赖管理器(IOC 容器)控制各个模块。
一个典型的 IOC 容器例子如下
IOC 容器是可以嵌套的,就像一个组件树一样,IOC 容器 的 dependencies 可以是一个 IOC 容器,然后它又有自己的 dependencies。
我写了一个非常简单的 ioc 容器 https://github.com/chunpu/minioc/blob/master/example/example.ts, 越简单越容易理解哦~
总结
依赖倒置,依赖注入,控制反转都是为了解耦。