Coding Hao

软件设计之控制反转(IoC)和依赖注入(DI)

2024年8月5日 (1年前)

简介

控制反转(Inversion of Control, IoC) 和 依赖注入(Dependency Injection, DI) 是软件设计中的两个重要概念,主要用于解耦代码、提高可维护性和可测试性。它们经常被一起讨论,但有不同的侧重点。

控制反转(IoC)

控制反转是一种设计原则,它将程序的控制权从传统的“程序员编写的代码”转移到“框架或容器”中。换句话说,程序的控制流不再由开发者直接管理,而是由框架或容器来管理。它颠倒了程序的控制流。传统程序中,代码直接控制所需组件的创建和使用;而在 IoC 中,这种控制权被转移到外部系统(通常是框架)。简单说,就是"不要打电话给我们,我们会打电话给你"。

核心思想

  • 传统控制流:开发者直接调用库或函数,控制权在开发者手中。

  • 控制反转:框架或容器负责调用开发者编写的代码,控制权在框架手中。

举例

  • 事件驱动编程:在 GUI 应用中,开发者编写事件处理函数,但何时调用这些函数由框架决定。
  • 模板方法模式:父类定义算法框架,子类实现具体步骤,控制权在父类。

依赖注入(DI)

依赖注入是控制反转的一种实现方式。它的核心思想是:类不应该创建依赖的对象,而应该通过构造函数、方法参数或属性接收它们。这使得类与其依赖解耦,便于测试和维护。

核心思想

  • 传统方式:对象内部直接创建或查找依赖,导致代码耦合度高。

  • 依赖注入:依赖由外部容器或框架提供,对象只需声明它需要什么, 将依赖的创建和管理从类内部转移到外部容器。通过构造函数、属性或方法注入依赖。

IoC 和 DI 的关系

IoC 是原则:控制反转是一个更广泛的概念,依赖注入是实现它的具体方式之一。

DI 是技术:依赖注入是实现控制反转的一种手段,其他实现方式还包括服务定位器(Service Locator)等。

Nest.JS 中的控制反转和依赖注入

控制反转(IoC)在 NestJS 中的体现

NestJS 是一个基于 IoC 原则的框架,它将应用程序的控制权从开发者手中转移到框架中。具体表现如下:

  • 模块化设计:NestJS 使用模块(Module)来组织应用程序,每个模块可以声明自己的提供者(Provider)、控制器(Controller)和导入其他模块。

  • 依赖管理:NestJS 的 IoC 容器负责管理所有依赖关系,开发者只需声明依赖,框架会自动解决和注入。

依赖注入(DI)在 NestJS 中的实现

NestJS 的依赖注入机制是其 IoC 实现的核心部分。它通过装饰器和元数据来自动解析和注入依赖。 关键概念

  • 提供者(Provider):

    • 提供者是 NestJS 中用于封装业务逻辑的类(如服务、仓库、工厂等)。

    • 通过 @Injectable() 装饰器标记为可注入的类。 示例:

      @Injectable()
      export class UserService {
      constructor(private readonly userRepository: UserRepository) {}
      }
  • 控制器(Controller):

    • 控制器负责处理 HTTP 请求,并将业务逻辑委托给提供者。

    • 通过 @Controller() 装饰器标记。 示例:

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

    • 模块是 NestJS 应用程序的组织单元,用于声明提供者、控制器和导入其他模块。

    • 通过 @Module() 装饰器定义。 示例:

      @Module({
      controllers: [UserController],
         providers: [UserService, UserRepository],
      })
      export class UserModule {}

依赖注入的工作流程

在构造函数或属性中声明依赖,NestJS 会自动解析并注入:

@Injectable()
export class UserService {
   constructor(private readonly userRepository: UserRepository) {}
}

在模块的 providers 数组中注册提供者:

 @Module({
   providers: [UserService, UserRepository],
 })
 export class UserModule {}

NestJS 的 IoC 容器会根据构造函数或属性的类型,自动注入对应的实例:

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

依赖注入的优势

  • 解耦:业务逻辑与依赖的具体实现分离。

  • 可测试性:依赖可以轻松替换为模拟对象(Mock),便于单元测试。

  • 可维护性:依赖关系清晰,易于修改和扩展。

依赖指的是什么:依赖(Dependency) 是指一个类、模块或组件需要另一个类、模块或组件来完成其功能。换句话说,如果一个对象 A 需要使用对象 B (无论是作为成员变量、方法参数还是在方法内部创建的对象)的功能,那么对象 A 就依赖于对象 B。依赖关系会导致代码耦合。高度耦合的代码难以测试和维护,因为修改一个组件可能会影响到依赖它的其他组件。依赖注入正是用来解决这个问题,通过将依赖关系从代码内部转移到外部,使各组件更加独立和可替换。

1. 依赖的常见形式

类依赖: UserService 依赖于 UserRepository。

class UserService {
constructor(private readonly userRepository: UserRepository) {}
}

模块依赖:

// UserModule 依赖于 DatabaseModule。
@Module({
    imports: [DatabaseModule],
})
export class UserModule {}

函数依赖: 一个函数需要另一个函数或服务来完成其逻辑。

// getUser 函数依赖于 UserRepository。
function getUser(id: number, userRepository: UserRepository) {
    return userRepository.findUserById(id);
}

2. 依赖的类型

强依赖: 直接在代码中创建依赖的实例,导致紧耦合。

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

弱依赖(依赖注入):通过外部注入依赖,实现松耦合。

@Injectable()
class UserService {
    constructor(private readonly userRepository: UserRepository) {} // 弱依赖: 通过构造函数注入依赖。
}

依赖注入的实现方式

// 构造函数注入:过构造函数传递依赖。
class UserService {
    constructor(private readonly userRepository: UserRepository) {}
}

// 属性注入
class UserService {
    @Inject()
    private userRepository: UserRepository;
}

// 方法注入
class UserService {
    private userRepository: UserRepository;
    setUserRepository(userRepository: UserRepository) {
        this.userRepository = userRepository;
    }
}