简介
控制反转(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;
}
}