一、概念
先看看概念释义(百度)- 依赖反转原则:
在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。
我第一个想法就是反转什么?再看看释义(chatgpt):
“反转” 指的是将依赖关系从具体实现转向抽象,这样高层模块就不会直接依赖低层模块的实现细节,而是依赖于抽象接口或抽象类。依赖注入则是实现这一反转原则的一个具体手段,通过将依赖项注入到类中,减少了类与其依赖项之间的耦合。
结合给的例子,虽然是 java 的例子,不过大同小异。
传统实现:
public class UserService {
private UserRepository userRepository;
public UserService() {
this.userRepository = new UserRepository(); // 依赖于具体实现
}
public void someServiceMethod() {
userRepository.someMethod();
}
}
应用依赖反转原则的实现:
// 定义抽象
public interface UserRepository {
void someMethod();
}
// 实现具体细节
public class UserRepositoryImpl implements UserRepository {
public void someMethod() {
// 实现细节
}
}
// 高层模块依赖于抽象
public class UserService {
private UserRepository userRepository;
// 依赖注入
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void someServiceMethod() {
userRepository.someMethod();
}
}
那么依赖注入又是什么,如果接触过 angular 的话,应该会知道这个概念。释义(chatgpt):
依赖注入是一种实现依赖反转的方法,通过将依赖项(如 UserRepository)注入到 UserService 中,而不是由 UserService 自行创建依赖项。依赖注入可以通过多种方式实现,例如:
- 构造器注入:通过构造函数将依赖项注入类。
- 属性注入:通过类的属性(setter 方法)注入依赖项。
- 接口注入:通过实现特定接口注入依赖项(这种方式在实践中不太常见)。
个人理解:依赖反转是一种编程思想,而依赖注入是其原则的一种实现方式。反转了对依赖的使用。
- 传统:直接在类里面声明使用一个依赖。如果需求上要改动依赖,就得改动调用方,或者重写一个新的调用方,去调用新的一个依赖。
- 依赖反转:给调用方传递一个依赖,大家约定好依赖要实现某个接口(方法),调用方就直接用。如此一来,如果需要更改依赖方法,就传递一个新的依赖进去就好。调用方的代码就不用改。
比如:跑自动化测试和正式使用,就可以给某些功能注入不同的 service,来达到适配不同环境。
个人觉得叫依赖注入还好理解点。不过一个是指导思想,一个是实施方案嘛。
二、IoC 容器
释义:
IoC(Inversion of Control,控制反转)容器是一个用于实现依赖注入的框架。它负责管理对象的创建和依赖关系的注入,从而实现依赖反转原则。IoC 容器自动将依赖项注入到对象中,简化了对象的管理和配置。
其中,inversify 就是前端的 IoC 容器,也有大佬给的中文文档。
用得也不多,记录下一些用法,大概相当于我使用的 nginx 冰山一角之于 nginx 全家桶吧。:)
官网示例:
import "reflect-metadata";
import { Container, injectable } from "inversify";
@injectable()
class Katana {
public hit() {
return "cut!";
}
}
@injectable()
class Shuriken {
public throw() {
return "hit!";
}
}
@injectable()
class Ninja implements Ninja {
private _katana: Katana;
private _shuriken: Shuriken;
private id: number;
public constructor(katana: Katana, shuriken: Shuriken) {
this.id = 1;
this._katana = katana;
this._shuriken = shuriken;
}
public fight() { return this._katana.hit(); };
public sneak() { return this._shuriken.throw(); };
public run() { this.id += 1; return this.id; };
}
// 容器默认是 inTransientScope { defaultScope: 'transient' }
var container = new Container({ autoBindInjectable: true });
// inTransientScope:每次解析时创建一个新实例,适合无状态服务或需要完全独立的服务。
// inSingletonScope:应用生命周期内只有一个实例,适合全局共享的服务。
// inRequestScope:每个请求创建一个新实例,但在同一请求内共享实例,适合请求范围内共享状态的服务。
container.bind(Ninja).toSelf().inTransientScope();
container.bind(Katana).toSelf();
container.bind(Shuriken).toSelf();
const ninja = container.get(Ninja);
console.log(ninja.run());
console.log(ninja.fight())
console.log(ninja.sneak())
// get 的时候就会创建实例,为了避免太看不出区别,这里延迟获取
// 可以看出不同模式下 id 不一样
setTimeout(() => {
const ninja1 = container.get(Ninja);
console.log(ninja1.run())
}, 1000)
一些使用要点
- 关于
reflect-metadata
,可以看看大佬的文章,Reflect。 - 装饰器 injectable,结合 reflect,可以简单理解,就是挂载一些元数据在对象上面。我理解也像是把这个抽象类给”注册“了吧。
- Container 就是声明一个池子,和要”注册“过的抽象类声明绑定一下。在构造器参数里引入这些抽象类,就会变成是 new 一个实例来使用。
new Container({ autoBindInjectable: true })
,有了这个可以不显性bind
,调用的get
方法的时候就可以把inject
过的抽象类里 new 一个实例,并且自动 bind 上。
其他问题
我们项目用的是 react + vite,如果构造器在引用参数的时候,是像上面代码那样引入,没有用 inject
装饰的话,会报一个错误:Missing required @inject or @multiInject annotation in: argument 0 in class Ninja.
后面试了传统的 webpack 也会,就是说:
// 简写会报错
class Ninja implements Ninja {
public constructor(katana: Katana, shuriken: Shuriken) {}
}
// 改成用 inject 装饰器就可以
class Ninja implements Ninja {
public constructor(@inject(Katana) katana: Katana, @inject(Shuriken) shuriken: Shuriken) {}
}
但是官网给的例子就是有不用属性装饰器的,而且用 ts-node
去启动也没有报错。所以可能是 esbuild、babel 对 typescript 装饰器什么的支持不好。这里可以改成 swc 去处理打包,webpack 的没研究了,vite 的话可以用这个插件,unplugin-swc,如示例用法就行:
import swc from 'unplugin-swc';
export default {
plugins: [swc.vite()],
};