切換語言為:簡體

如何在Node.js裏實現依賴注入

  • 爱糖宝
  • 2024-10-11
  • 2040
  • 0
  • 0

什麼是依賴注入

依賴注入是一種用於在開發過程中實現控制反轉(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;
}

優先從快取里加載,如果快取不存在則主動去載入一次。

第三方庫

除了自己實現之外,我們也可以藉助第三方的庫,如InversifyJSAwilix等。這些庫提供了更高階的功能,如依賴的自動解析、生命週期管理等。下面是使用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的裝飾器來標記LoggerEmailService是可注入的。我們還建立了一個Container來管理我們的依賴,然後從容器中獲取了EmailService的例項。

總結

依賴注入是一個強大的模式,它可以幫助我們構建更加靈活、可維護和可測試的Node.js應用程式。無論是手動實現還是使用專門的庫,依賴注入都值得在我們的工具箱中佔有一席之地。透過將依賴注入作為應用程式架構的一部分,我們可以提高程式碼質量,併爲未來的擴充套件打下堅實的基礎。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.