什麼是依賴注入
依賴注入是一種用於在開發過程中實現控制反轉(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應用程式。無論是手動實現還是使用專門的庫,依賴注入都值得在我們的工具箱中佔有一席之地。透過將依賴注入作為應用程式架構的一部分,我們可以提高程式碼質量,併爲未來的擴充套件打下堅實的基礎。