什么是依赖注入
依赖注入是一种用于在开发过程中实现控制反转(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应用程序。无论是手动实现还是使用专门的库,依赖注入都值得在我们的工具箱中占有一席之地。通过将依赖注入作为应用程序架构的一部分,我们可以提高代码质量,并为未来的扩展打下坚实的基础。