什么是依赖注入
依赖注入是一种用于在开发过程中实现控制反转(IoC)的技术。在IoC中,对程序流的控制是颠倒的:依赖项不是控制其依赖项的创建和管理的组件,而是从外部源提供给组件。
在传统的编程模式中,一个组件可能会直接创建并管理它所依赖的其他组件,这会导致组件之间的耦合度较高,难以维护和测试。
控制反转是一种设计原则,它改变了组件之间的控制关系。在IoC中,组件不再自己创建和管理它所依赖的组件,而是将这种控制权交给外部。具体来说,依赖注入是IoC的一种实现方式,它通过外部源(比如容器或框架)来提供组件所需的依赖项。
这样做的好处是:
-
解耦:组件不再直接依赖于具体的依赖项实现,而是依赖于抽象的接口或抽象类,这样可以降低组件之间的耦合度。 -
易于维护:由于组件之间的依赖关系是由外部控制的,因此修改一个组件的依赖项时,不需要修改组件本身的代码,只需要调整外部的配置或代码。 -
易于测试:在单元测试时,可以轻松地替换组件的依赖项为模拟对象(mock objects),从而可以独立地测试组件的功能。 -
可重用性:由于组件不直接依赖于具体的实现,而是依赖于抽象,这使得组件更容易在不同的上下文中被重用。
如何实现
了解完定义,我们来看一下案例。先看一个没有使用依赖注入的例子:
手动注入
// Dependency.js
class Dependency {
constructor() {
this.name = 'Dependency';
}
}
// Service.js
class Service {
constructor(dependency) {
this.dependency = dependency;
}
greet() {
console.log(`Hello, I depend on ${this.dependency.name}`);
}
}
// App.js
const Dependency = require('./Dependency');
const Service = require('./Service');
const dependency = new Dependency();
const service = new Service(dependency);
service.greet();
这里展示了一个简单的依赖注入模式。Service
依赖于dependency
对象,在创建了Service
类的实例时,将dependency
实例作为参数传递给Service
的构造函数,这样Service
就依赖于Dependency
。
自动注入
手动注入毕竟太麻烦,而且依赖的实例多的时候,每个都通过形参传入不太靠谱,下面我们来看看如何实现自动注入。
// Dependency.js
export class Dependency {
constructor() {
this.name = 'Dependency';
}
}
// Service.js
export class Service {
constructor(dependency) {
this.dependency = dependency;
}
greet() {
console.log(`Hello, I depend on ${this.dependency.name}`);
}
}
// Container.js
import { Dependency } from './Dependency';
import { Service } from './Service';
export class Container {
constructor() {
this.dependencyInstances = new Map();
this.dependencyConstructors = new Map([
[Dependency, Dependency],
[Service, Service],
]);
}
getDependency(ctor) {
if (!this.dependencyInstances.has(ctor)) {
const dependencyConstructor = this.dependencyConstructors.get(ctor);
if (!dependencyConstructor) {
throw new Error(`No dependency registered for ${ctor.name}`);
}
const instance = new dependencyConstructor(this.getDependency.bind(this));
this.dependencyInstances.set(ctor, instance);
}
return this.dependencyInstances.get(ctor);
}
}
// App.js
import { Container } from './Container';
import { Service } from './Service';
import { Dependency } from './Dependency';
const container = new Container();
const service = container.getDependency(Service);
service.greet();
这里增加了Container
用于管理实例,我们只需要维护对应的依赖关系,在需要使用的时候再创建对应的实例。是不是很简单?简单才是王道,使用过egg的小伙伴都知道egg里只需要导出Class,我们就可以直接在context里访问对应的实例。
// app/controller/user.js
const Controller = require('egg').Controller;
class UserController extends Controller {
async info() {
const { ctx } = this;
const userId = ctx.params.id;
const userInfo = await ctx.service.user.find(userId);
ctx.body = userInfo;
}
}
module.exports = UserController;
// app/service/user.js
const Service = require('egg').Service;
class UserService extends Service {
async find(uid) {
// 假如我们拿到用户 id,从数据库获取用户详细信息
const user = await this.ctx.db.query(
'select * from user where uid = ?',
uid
);
// 假定这里还有一些复杂的计算,然后返回需要的信息
const picture = await this.getPicture(uid);
return {
name: user.user_name,
age: user.age,
picture
};
}
}
module.exports = UserService;
egg里的实现其实更彻底,直接使用了getter替代了container.getDependency(Service)
,使用了本地文件读取加载class实例。其实现如下:
// define ctx.service
Object.defineProperty(app.context, property, {
get() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const ctx = this;
// distinguish property cache,
// cache's lifecycle is the same with this context instance
// e.x. ctx.service1 and ctx.service2 have different cache
if (!ctx[CLASS_LOADER]) {
ctx[CLASS_LOADER] = new Map();
}
const classLoader: Map<string | symbol, ClassLoader> = ctx[CLASS_LOADER];
let instance = classLoader.get(property);
if (!instance) {
instance = getInstance(target, ctx);
classLoader.set(property, instance!);
}
return instance;
},
});
优先从缓存里读取实例,不存在则执行getInstance,其实现如下:
function getInstance(values: any, ctx: ContextDelegation) {
// it's a directory when it has no exports
// then use ClassLoader
const Class = values[EXPORTS] ? values : null;
let instance;
if (Class) {
if (isClass(Class)) {
instance = new Class(ctx);
} else {
// it's just an object
instance = Class;
}
// Can't set property to primitive, so check again
// e.x. module.exports = 1;
} else if (isPrimitive(values)) {
instance = values;
} else {
instance = new ClassLoader({ ctx, properties: values });
}
return instance;
}
优先从缓存里加载,如果缓存不存在则主动去加载一次。
第三方库
除了自己实现之外,我们也可以借助第三方的库,如InversifyJS
、Awilix
等。这些库提供了更高级的功能,如依赖的自动解析、生命周期管理等。下面是使用InversifyJS
的一个基本示例:
首先,安装InversifyJS
:
npm install inversify reflect-metadata --save
然后,我们可以这样使用它:
const { injectable, inject, Container } = require('inversify');
require('reflect-metadata');
// 定义依赖
@injectable()
class Logger {
log(message) {
console.log(message);
}
}
@injectable()
class EmailService {
constructor(@inject(Logger) logger) {
this.logger = logger;
}
sendEmail(to, content) {
// 发送邮件的逻辑...
this.logger.log(`Sending email to ${to}`);
}
}
// 设置容器
const container = new Container();
container.bind(Logger).toSelf();
container.bind(EmailService).toSelf();
// 从容器中获取实例
const emailService = container.get(EmailService);
// 使用服务
emailService.sendEmail('example@example.com', 'Hello, Dependency Injection with InversifyJS!');
在这个例子中,我们使用了InversifyJS
的装饰器来标记Logger
和EmailService
是可注入的。我们还创建了一个Container
来管理我们的依赖,然后从容器中获取了EmailService
的实例。
总结
依赖注入是一个强大的模式,它可以帮助我们构建更加灵活、可维护和可测试的Node.js应用程序。无论是手动实现还是使用专门的库,依赖注入都值得在我们的工具箱中占有一席之地。通过将依赖注入作为应用程序架构的一部分,我们可以提高代码质量,并为未来的扩展打下坚实的基础。
本文由 mdnice 多平台发布