解释 JavaScript 中 Dependency Injection (依赖注入) 模式在大型应用中的作用,并举例说明其在 Angular 或 NestJS 中的实现。

JavaScript 依赖注入:大型应用的救星 (Angular/NestJS 实践)

大家好!欢迎来到今天的 JavaScript 依赖注入 (DI) 讲座。我是你们的老朋友,江湖人称“代码老中医”,专治各种大型应用“代码臃肿、难以测试、牵一发动全身”的疑难杂症。今天,咱们就来聊聊 DI 这个能让你的代码变得更加灵活、可维护、可测试的“灵丹妙药”。

什么是依赖? 什么是依赖注入?

在开始之前,我们先来搞清楚什么是“依赖”。

想象一下你开了一家咖啡馆,需要各种原料才能制作咖啡,比如咖啡豆、牛奶、糖浆等等。你的咖啡馆就“依赖”这些原料才能正常运转。如果哪天咖啡豆供应商罢工了,你的咖啡馆就没法制作咖啡了,这就是“依赖”带来的问题。

在编程世界里,“依赖”指的是一个类或模块需要另一个类或模块才能正常工作。例如,一个 UserService 类可能需要 UserRepository 类来访问数据库。

class UserRepository {
  getUserById(id) {
    // 从数据库获取用户
    return { id: id, name: '张三' };
  }
}

class UserService {
  constructor() {
    this.userRepository = new UserRepository(); // UserService 依赖 UserRepository
  }

  getUser(id) {
    return this.userRepository.getUserById(id);
  }
}

const userService = new UserService();
const user = userService.getUser(1);
console.log(user); // { id: 1, name: '张三' }

上面的例子中,UserService 直接创建了 UserRepository 的实例,这就产生了紧耦合。如果有一天你想换一个 UserRepository 的实现,比如使用不同的数据库,你就需要修改 UserService 的代码。

这时候,依赖注入就闪亮登场了!它就像一个“媒婆”,负责把 UserService 需要的 UserRepository “嫁”给它,而不是让 UserService 自己去“找对象”。

依赖注入 (Dependency Injection, DI) 的定义

DI 是一种设计模式,其核心思想是:不要让类自己去创建或查找它所依赖的对象,而是通过外部容器将依赖项“注入”到类中。

简单来说,就是把“创建依赖”的责任从类自身转移到外部,让类只专注于自己的业务逻辑。

DI 的好处:

  • 解耦合 (Decoupling): 类不再直接依赖于具体的实现,而是依赖于接口或抽象类。这使得你可以轻松地替换不同的实现,而无需修改类的代码。
  • 可测试性 (Testability): 可以方便地使用 Mock 对象或 Stub 对象来模拟依赖项,从而更容易地对类进行单元测试。
  • 可维护性 (Maintainability): 代码更加模块化,易于理解和修改。
  • 可复用性 (Reusability): 类可以被不同的应用程序或模块复用。

DI 的实现方式:

DI 主要有三种实现方式:

  1. 构造函数注入 (Constructor Injection): 通过类的构造函数来注入依赖项。这是最常用的方式。
  2. Setter 注入 (Setter Injection): 通过 Setter 方法来注入依赖项。
  3. 接口注入 (Interface Injection): 通过接口定义注入方法来注入依赖项 (不常用)。

接下来,我们用代码示例来演示这三种方式。

构造函数注入 (Constructor Injection)

// 定义 UserRepository 接口
class IUserRepository {
  getUserById(id) {
    throw new Error('Method not implemented.');
  }
}

// UserRepository 的具体实现
class UserRepository extends IUserRepository {
  getUserById(id) {
    // 从数据库获取用户
    return { id: id, name: '张三 (来自数据库)' };
  }
}

// UserService 使用构造函数注入 UserRepository
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  getUser(id) {
    return this.userRepository.getUserById(id);
  }
}

// 创建 UserRepository 实例
const userRepository = new UserRepository();

// 通过构造函数注入 UserRepository 到 UserService
const userService = new UserService(userRepository);

const user = userService.getUser(1);
console.log(user); // { id: 1, name: '张三 (来自数据库)' }

// 单元测试示例:使用 Mock UserRepository
class MockUserRepository extends IUserRepository {
  getUserById(id) {
    return { id: id, name: '测试用户 (来自 Mock)' };
  }
}

const mockUserRepository = new MockUserRepository();
const userServiceWithMock = new UserService(mockUserRepository);
const userFromMock = userServiceWithMock.getUser(1);
console.log(userFromMock); // { id: 1, name: '测试用户 (来自 Mock)' }

Setter 注入 (Setter Injection)

// 定义 UserRepository 接口
class IUserRepository {
  getUserById(id) {
    throw new Error('Method not implemented.');
  }
}

// UserRepository 的具体实现
class UserRepository extends IUserRepository {
  getUserById(id) {
    // 从数据库获取用户
    return { id: id, name: '李四 (来自数据库)' };
  }
}

// UserService 使用 Setter 注入 UserRepository
class UserService {
  constructor() {
    this.userRepository = null;
  }

  setUserRepository(userRepository) {
    this.userRepository = userRepository;
  }

  getUser(id) {
    if (!this.userRepository) {
      throw new Error('UserRepository 未设置');
    }
    return this.userRepository.getUserById(id);
  }
}

// 创建 UserRepository 实例
const userRepository = new UserRepository();

// 创建 UserService 实例
const userService = new UserService();

// 使用 Setter 方法注入 UserRepository
userService.setUserRepository(userRepository);

const user = userService.getUser(2);
console.log(user); // { id: 2, name: '李四 (来自数据库)' }

接口注入 (Interface Injection)

// 定义注入接口
class IUserRepositoryInjector {
  setUserRepository(userRepository) {
    throw new Error('Method not implemented.');
  }
}

// 定义 UserRepository 接口
class IUserRepository {
  getUserById(id) {
    throw new Error('Method not implemented.');
  }
}

// UserRepository 的具体实现
class UserRepository extends IUserRepository {
  getUserById(id) {
    // 从数据库获取用户
    return { id: id, name: '王五 (来自数据库)' };
  }
}

// UserService 实现了注入接口
class UserService extends IUserRepositoryInjector {
  constructor() {
    super();
    this.userRepository = null;
  }

  setUserRepository(userRepository) {
    this.userRepository = userRepository;
  }

  getUser(id) {
    if (!this.userRepository) {
      throw new Error('UserRepository 未设置');
    }
    return this.userRepository.getUserById(id);
  }
}

// 创建 UserRepository 实例
const userRepository = new UserRepository();

// 创建 UserService 实例
const userService = new UserService();

// 使用接口方法注入 UserRepository
userService.setUserRepository(userRepository);

const user = userService.getUser(3);
console.log(user); // { id: 3, name: '王五 (来自数据库)' }

依赖注入容器 (Dependency Injection Container)

在大型应用中,手动管理依赖项会变得非常繁琐。这时候,我们就需要一个“依赖注入容器”来帮助我们管理依赖项的创建和注入。

依赖注入容器是一个框架或库,它可以自动地创建和注入依赖项。它通常提供以下功能:

  • 注册 (Registration): 注册类及其依赖项。
  • 解析 (Resolution): 根据类的依赖关系,自动创建实例并注入依赖项。
  • 生命周期管理 (Lifecycle Management): 管理依赖项的生命周期,例如单例模式、瞬态模式等。

Angular 中的依赖注入

Angular 框架内置了强大的依赖注入系统。它使用 TypeScript 的装饰器 (Decorators) 来声明依赖关系,并使用依赖注入容器来管理依赖项。

import { Injectable } from '@angular/core';

// 定义 UserService
@Injectable({
  providedIn: 'root' // 声明 UserService 为可注入的服务,并指定在根模块中提供
})
export class UserService {
  constructor(private userRepository: UserRepository) { // 构造函数注入 UserRepository
  }

  getUser(id: number) {
    return this.userRepository.getUserById(id);
  }
}

// 定义 UserRepository
@Injectable({
  providedIn: 'root' // 声明 UserRepository 为可注入的服务,并指定在根模块中提供
})
export class UserRepository {
  getUserById(id: number) {
    // 从数据库获取用户
    return { id: id, name: '赵六 (来自 Angular)' };
  }
}

// 在组件中使用 UserService
import { Component } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user',
  template: `
    <p>User ID: {{ user?.id }}</p>
    <p>User Name: {{ user?.name }}</p>
  `
})
export class UserComponent {
  user: any;

  constructor(private userService: UserService) { // 构造函数注入 UserService
    this.user = this.userService.getUser(4);
  }
}

Angular DI 的关键概念:

  • @Injectable() 装饰器: 用于声明一个类为可注入的服务。providedIn 属性指定了服务在哪个模块中提供。'root' 表示在根模块中提供,这意味着整个应用程序都可以访问该服务。
  • 构造函数注入: Angular 使用构造函数注入来自动注入依赖项。
  • providers 数组: 在模块或组件的 providers 数组中可以指定依赖项的提供者。
  • useClass, useValue, useFactory, useExisting: 这些属性用于配置依赖项的提供方式。
属性 描述 示例
useClass 使用一个类来提供依赖项。这是最常用的方式。 typescript providers: [{ provide: IUserRepository, useClass: UserRepository }]
useValue 使用一个静态值来提供依赖项。 typescript providers: [{ provide: 'API_URL', useValue: 'https://api.example.com' }]
useFactory 使用一个工厂函数来提供依赖项。这可以用于创建复杂的依赖项或根据条件创建不同的依赖项。 typescript providers: [{ provide: UserService, useFactory: (userRepository: UserRepository) => { return new UserService(userRepository); }, deps: [UserRepository] }] 注意 deps 数组指定了工厂函数所需的依赖项。
useExisting 使用一个已存在的令牌来提供依赖项。这可以用于创建一个别名或将一个依赖项绑定到另一个依赖项。 typescript providers: [{ provide: AnotherService, useExisting: UserService }] AnotherService 现在会指向 UserService 的实例。

NestJS 中的依赖注入

NestJS 是一个基于 Node.js 的渐进式框架,用于构建高效、可靠和可扩展的服务器端应用程序。它深受 Angular 的影响,也内置了强大的依赖注入系统。

import { Injectable, Inject } from '@nestjs/common';

// 定义 UserService
@Injectable()
export class UserService {
  constructor(
    @Inject('UserRepository') private readonly userRepository: IUserRepository, // 构造函数注入 UserRepository,使用 @Inject 装饰器指定令牌
  ) {}

  getUser(id: number) {
    return this.userRepository.getUserById(id);
  }
}

// 定义 UserRepository 接口
export interface IUserRepository {
  getUserById(id: number): any;
}

// 定义 UserRepository
@Injectable()
export class UserRepository implements IUserRepository {
  getUserById(id: number): any {
    // 从数据库获取用户
    return { id: id, name: '钱七 (来自 NestJS)' };
  }
}

// 在 Module 中注册 providers
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
import { IUserRepository } from './interfaces/user-repository.interface';

@Module({
  providers: [
    UserService,
    {
      provide: 'UserRepository', // 使用令牌 'UserRepository'
      useClass: UserRepository,
    },
  ],
  exports: [UserService], // 导出 UserService,以便其他模块可以使用
})
export class UserModule {}

// 在 Controller 中使用 UserService
import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  getUser(@Param('id') id: string) {
    return this.userService.getUser(parseInt(id));
  }
}

NestJS DI 的关键概念:

  • @Injectable() 装饰器: 用于声明一个类为可注入的服务。
  • @Inject() 装饰器: 用于指定要注入的依赖项的令牌 (Token)。
  • providers 数组: 在模块的 providers 数组中可以指定依赖项的提供者。
  • 令牌 (Token): 用于标识依赖项的唯一标识符。可以是字符串、类或符号 (Symbol)。
  • useClass, useValue, useFactory: 与 Angular 类似,用于配置依赖项的提供方式。

DI 容器的原理 (简化版):

DI 容器的核心是一个映射表,它存储了令牌 (Token) 和提供者 (Provider) 之间的对应关系。

当需要解析一个依赖项时,DI 容器会根据令牌在映射表中查找对应的提供者,然后使用提供者来创建依赖项的实例并注入到目标类中。

DI 的注意事项:

  • 过度使用 DI: DI 是一种强大的工具,但不要过度使用。对于简单的应用,手动管理依赖项可能更简单。
  • 循环依赖 (Circular Dependency): 避免出现循环依赖,例如 A 依赖于 B,B 又依赖于 A。DI 容器通常会检测并抛出异常。
  • 依赖注入框架的选择: 选择适合你的项目和团队的依赖注入框架。

总结:

依赖注入是一种重要的设计模式,可以提高代码的灵活性、可测试性和可维护性。在大型 JavaScript 应用中,使用依赖注入容器可以更好地管理依赖项,降低代码的复杂性。Angular 和 NestJS 都内置了强大的依赖注入系统,可以帮助你轻松地实现依赖注入。

希望今天的讲座能帮助你更好地理解 JavaScript 中的依赖注入。记住,代码就像人,需要精心呵护才能健康成长。依赖注入就是一种“呵护”代码的方式,让你的代码更加健壮、易于维护,最终为你省下大把的加班时间!

好了,今天的讲座就到这里。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注