控制反转和依赖倒置原则特别适用于需要提高代码灵活性、可维护性和可测试性的场景,如大型应用程序和框架开发。合理使用可以降低耦合度,提高代码可维护性、可测试性和可扩展性,促进模块化、灵活性,使系统更易于理解、修改和扩展。
控制反转原则在很多注明的软件中都有使用,其中最普遍的就是插件
- 例如VSCode中可以安装各种插件实现所有语言的高亮等
- chrome浏览器可以安装插件来管理标签、操作Cookie、分析页面内容(如翻译)等等
使用好控制反转可以让我们的代码降低耦合,条理更加清晰。
控制反转和依赖倒置原则是软件设计中的两个重要概念,它们可以帮助我们编写更加灵活、可维护和可扩展的代码。以下是合理使用控制反转、依赖倒置的好处:
-
松耦合 :
控制反转和依赖倒置原则有助于降低组件之间的耦合度。通过将依赖关系从组件内部移动到外部,我们可以实现组件之间的松耦合,从而使得组件更加独立、可复用和易于测试。这样的设计使得系统更加灵活,可以更容易地进行修改和扩展。
-
可维护性 :
通过使用控制反转和依赖倒置原则,我们可以将代码组织得更加清晰和模块化,从而提高代码的可读性和可维护性。依赖关系的明确声明使得代码的逻辑更加清晰,可以更容易地理解和修改。
-
可测试性 :
控制反转和依赖倒置原则使得代码更容易进行单元测试和集成测试。通过将依赖关系注入到组件中,我们可以使用模拟对象来模拟依赖组件的行为,从而更容易地编写测试用例,并且减少了对外部资源的依赖。
-
可扩展性 :
使用控制反转和依赖倒置原则可以使系统更容易进行扩展。当系统需要添加新的功能或者修改现有功能时,我们可以通过替换依赖组件或者添加新的依赖来实现,而不需要修改现有的代码。这样的设计使得系统更加灵活,可以更容易地应对变化和需求的变更。
一、依赖倒置
在理解 控制反转 之前,我们需要先理解 依赖倒置
依赖倒置实质上是面向接口编程的体现,业务开发者不需要感知到系统的实现细节,只需要感知系统暴露出来的接口即可
案例1:
正常写代码就像亲自开车:
- 例如汽车A的方向盘在左侧,汽车B的方向盘在右侧,你可能还有其他更多的汽车,你需要知道每一辆汽车的使用方法
- 且如果这个车有升级改款后,你还要感知到它的变化,不然可能就无法使用
依赖倒置就像乘坐公共交通工具(如公交车):
- 你不需要关心不同种类的公交车如何驾驶,只需要知道乘坐公交车的规则即可(如到站台等车、上车投币、在目标站台下车),乘坐所有公交车的规则都一样
- 如果公交车本身有升级,你也无需感知这件事
汽车表示业务逻辑、亲自开车时业务层的每次变动都有可能影响整个系统的稳定性,且这种变动通常是由团队中很多人来分别维护,非常不便于监控变更识别风险
而乘坐公交车的规则通常指IoC中的接口,通常是不会变更的,公交车(业务逻辑)的变化不会影响整个系统的稳定,只会影响单个业务的稳定性。
案例2
另一个开发组件的例子
- 写一个布局组件Layout,由 A B 2个业务组件分别实现 2 个功能放在布局的左右侧,A 和 B 组件需要不同的参数,那么我们可以这样实现:
const ComponentA = ({ name }: { name: string }) => {
return <>A: {name}</>
};
const ComponentB = ({ age }: { age: number }) => {
return <>B: {age}</>
};
const Layout = () => {
return (
<div className="layout">
<div className="left">
<ComponentA name="Tom" />
</div>
<div className="right">
<ComponentB age={24} />
</div>
</div>
);
}
const Page = () => {
<Layout />
};
- 可以看到此时 布局组件 依赖了 组件A 和 组件B,当组件A或B的属性发生变更时,布局组件也感知到,并且需要修改布局组件中传递给业务组件的参数。
- 这种依赖关系导致了业务层变更可能会导致系统的稳定性
使用控制反转的思想优化一下:
const ComponentA = ({ name }: { name: string }) => {
return <>A: {name}</>
};
const ComponentB = ({ age }: { age: number }) => {
return <>B: {age}</>
};
const Layout = ({ left, right }: { left?: ReactNode: right?: ReactNode }) => {
return (
<div className="layout">
<div className="left">{left}</div>
<div className="right">{right}</div>
</div>
);
}
const Page = () => {
return <Layout
left={<ComponentA name="Tom" />}
right={<ComponentB age={24} />}
/>
};
- 我们使用React的插槽能力优化了代码,可以看到现在 布局组件已经不需要依赖 业务组件了,而是变成了业务组件依赖了布局组件(依赖插槽)
- 这种依赖关系使业务组件变更时无需改动系统底层(Layout),只需要在业务层修改一下使用方式即可,保证了系统整体的稳定性
总结
- 从上面可以看出从底层依赖业务,使用IoC变成了业务依赖底层,使系统变得更加稳定
- 但是使用IoC也有风险, 使用IoC对接口的设计是有很高的要求的,需要灵活且保持稳定,万一发生变更就需要业务感知,最好不要出现 Breaking change
二、控制反转
控制反转(Inversion of Control,IoC)是一种软件设计思想,它主要是用来解耦组件之间的依赖关系,降低代码的耦合度,提高代码的可维护性和可扩展性。
概念
-
【依赖倒置】是面向对象开发领域中的软件设计原则,它倡导上层模块不依赖于底层模块,抽象不依赖细节。
-
【控制反转】是遵守依赖倒置这个原则而提出来的一种设计模式,它引入了 IoC 容器的概念。
-
【依赖注入】是为了实现依赖反转的一种手段之一。
-
它们的本质是为了代码更加的【高内聚,低耦合】
原则
- 上层模块不应该依赖底层模块,它们都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
三、依赖注入
IoC最常见的实现方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫依赖查找(Dependency Lookup)。
在传统的程序设计中,组件之间的依赖关系是硬编码在代码中的,一个组件直接依赖于另一个组件,导致这两个组件之间的耦合度很高。而控制反转则是将这种依赖关系的控制权从组件自身转移到外部容器(通常是一个框架或者容器),由容器来负责组件的创建、管理和装配,从而达到了降低耦合度的目的。
控制反转的核心思想是将控制权交给容器,让容器来决定组件之间的依赖关系,组件不再关心依赖对象的具体实现细节,而是通过接口或者抽象类来定义依赖关系,容器根据配置信息来注入具体的依赖对象。
Javascript 实现依赖注入的方式之一inversify
用法
备注:
- Warrior: 战士
- Weapon: 武器
- ThrowableWeapon: 可被投掷的武器
步骤 1: 声明接口和类型
- 先声明一些接口(抽象)
// file interfaces.ts
interface Warrior {
fight(): string;
sneak(): string;
}
interface Weapon {
hit(): string;
}
interface ThrowableWeapon {
throw(): string;
}
步骤 2: 使用 @injectable 和 @inject 装饰器声明依赖
- 在运行时使用类型标记作为标识符
// file types.ts
const TYPES = {
Warrior: Symbol.for("Warrior"),
Weapon: Symbol.for("Weapon"),
ThrowableWeapon: Symbol.for("ThrowableWeapon")
};
export { TYPES };
- 在构造函数中使用
@inject(Symbol)
即可注入对应的类
@injectable()
class Katana implements Weapon {
public hit = () => "cut!";
}
@injectable()
class Shuriken implements ThrowableWeapon {
public throw = () => "hit!";
}
@injectable()
class Ninja implements Warrior {
// 属性申明,下方构造函数注入
private _katana: Weapon;
// 属性注入
@inject(TYPES.ThrowableWeapon) private _shuriken: ThrowableWeapon;
// 构造函数注入
public constructor(@inject(TYPES.Weapon) katana: Weapon) {
this._katana = katana;
}
public fight() { return this._katana.hit(); }
public sneak() { return this._shuriken.throw(); }
}
步骤 3: 创建和配置容器
// file inversify.config.ts
import { Container } from "inversify";
import { TYPES } from "./types";
import { Warrior, Weapon, ThrowableWeapon } from "./interfaces";
import { Ninja, Katana, Shuriken } from "./entities";
const myContainer = new Container();
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja);
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana);
myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);
export { myContainer };
注意避免出现循环依赖
以下情况会出现循环依赖:
- A 依赖 B,B 也依赖 A
- A 依赖 B,B 依赖 C, C 又依赖 A
出现循环依赖会被inversify检测出来并报错。
- PS小插曲:
- 如何检测出现了循环依赖?查找树中是否有环,使用哈希表法(遍历树并记录已遍历的节点,出现重合集有环)
- 检测链表是否有环除了使用 哈希表法,还可以用 快慢指针法(一个指针每次走2格,一个每次走一格,如果重合即存在环)
四、总结
-
【依赖倒置】是面向对象开发领域中的软件设计原则,它倡导上层模块不依赖于底层模块,抽象不依赖细节。
-
【控制反转】是遵守依赖倒置这个原则而提出来的一种设计模式,它引入了 IoC 容器的概念。
-
【依赖注入】为了实现依赖反转的一种手段之一。
-
它们的本质是为了代码更加的“高内聚,低耦合”。
原文链接:https://juejin.cn/post/7352805162073063478 作者:前端君