依赖注入(Dependency Injection, DI)
依赖注入是一种设计模式,用于实现控制反转(Inversion of Control, IoC),它的核心思想是:对象不应该自己创建或查找它所依赖的其他对象,而应该由外部将这些依赖“注入”进来。
1. 问题背景:紧耦合的代码
先看一个没有使用依赖注入的例子:
ts
class EmailService {
send(to: string, message: string) {
console.log(`发送邮件到 ${to}: ${message}`);
}
}
class UserService {
private emailService = new EmailService(); // 紧耦合!
register(email: string) {
// 业务逻辑
this.emailService.send(email, "欢迎注册!");
}
}问题:
UserService和EmailService紧密耦合- 无法轻松替换邮件服务(比如换成短信服务)
- 难以测试(无法 mock
EmailService) - 违反了单一职责原则(UserService 既要处理用户逻辑,又要负责创建依赖)
2. 依赖注入的基本方案
构造函数注入(标准依赖注入)
ts
interface NotificationService {
send(to: string, message: string): void;
}
class EmailService implements NotificationService {
send(to: string, message: string) {
console.log(`📧 发送邮件到 ${to}: ${message}`);
}
}
class SMSService implements NotificationService {
send(to: string, message: string) {
console.log(`📱 发送短信到 ${to}: ${message}`);
}
}
// UserService 不再关心具体使用哪个服务
class UserService {
constructor(private notificationService: NotificationService) {}
register(email: string) {
this.notificationService.send(email, "欢迎注册!");
}
}
// 使用时注入依赖
const emailUser = new UserService(new EmailService());
const smsUser = new UserService(new SMSService());属性注入
setXXX 方法,注入外部依赖到实例属性。
ts
class UserService {
notificationService!: NotificationService;
setNotificationService(service: NotificationService) {
this.notificationService = service;
}
}方法注入
这里其实不算注入,应该叫依赖传递。
ts
class UserService {
register(email: string, notificationService: NotificationService) {
notificationService.send(email, "欢迎注册!");
}
}3. 依赖注入容器(DI Container)
手动管理依赖注入在复杂应用中会变得繁琐,这时就需要依赖注入容器,DI 容器的作用就是自动创建完整的依赖树。
场景:
ts
class DatabaseConnection {
connect() { /* ... */ }
}
class UserRepository {
constructor(private db: DatabaseConnection) {} // 依赖 DatabaseConnection
}
class UserService {
constructor(private repo: UserRepository) {} // 依赖 UserRepository
}
// 最终要创建 UserService,但需要自动创建:
// UserService ← UserRepository ← DatabaseConnection
// 1、手动创建实现
const db = new DatabaseConnection();
const repo = new UserRepository(db);
const service = new UserService(repo);
// 2、基于DI容器实现
const service = container.resolve<UserService>('UserService');关键问题:
1、如何自动化地在运行时获取某个类的构造函数参数信息?
ts
// 🔦 通过反射,或装饰器拿到构造函数元信息(参数类型信息)
function Injectable(): ClassDecorator {
return (target: any) => {
Reflect.defineMetadata('design:paramtypes', [...], target);
};
}
@Injectable()
class UserService {
constructor(private notificationService: NotificationService) {}
}2、针对递归依赖问题,需要递归解析并创建依赖
ts
resolve<T>(token: string): T {
const useClass = this.providers.get(token);
// 获取构造函数的参数类型列表
const paramTypes = Reflect.getMetadata('design:paramtypes', useClass);
// 递归解析每个依赖
const args = paramTypes.map((paramType: any) => {
const paramName = paramType.name; // 如 "NotificationService"
return this.resolve(paramName); // 递归创建依赖
});
return new useClass(...args); // 用解析好的依赖实例化
}3、支持实例的生命周期管理
singleton有的实例是单例的,整个应用只需要创建一次transient有的是瞬态的,每次resolve都需要创建scoped有的和作用域相关,比如每次 http 请求创建一个实例
ts
container.register('Database', DatabaseConnection, { scope: 'singleton' });简单模拟实现:
ts
class DIContainer {
private providers = new Map<string, Provider>();
// 注册服务
register<T>(
token: string | (new (...args: any[]) => T),
useClass?: new (...args: any[]) => T,
scope: Scope = 'singleton'
): void {
const key = typeof token === 'string' ? token : token.name;
const impl = useClass || (token as new (...args: any[]) => T);
this.providers.set(key, {
useClass: impl,
scope
});
}
// 解析服务(带循环依赖检测)
resolve<T>(token: string | (new (...args: any[]) => T)): T {
const key = typeof token === 'string' ? token : token.name;
const provider = this.providers.get(key);
if (!provider) {
throw new Error(`未注册的服务: ${key}`);
}
// 如果是单例且已创建,直接返回
if (provider.scope === 'singleton' && provider.instance) {
return provider.instance;
}
// 创建新实例
const instance = this.instantiate(provider.useClass, new Set());
// 缓存单例
if (provider.scope === 'singleton') {
provider.instance = instance;
}
return instance;
}
// 递归实例化(带循环依赖检测)
private instantiate<T>(target: new (...args: any[]) => T, visiting: Set<string>): T {
const typeName = target.name;
// 检测循环依赖
if (visiting.has(typeName)) {
throw new Error(`检测到循环依赖: ${Array.from(visiting).join(' → ')} → ${typeName}`);
}
visiting.add(typeName);
try {
// 获取构造函数参数类型(由 emitDecoratorMetadata 生成)
const paramTypes: (new (...args: any[]) => any)[] =
Reflect.getMetadata('design:paramtypes', target) || [];
// 递归解析每个依赖
const args = paramTypes.map(paramType => {
if (!paramType) {
throw new Error(`无法解析 ${typeName} 的某个依赖(可能是 any 或未启用 emitDecoratorMetadata)`);
}
return this.resolve(paramType);
});
return new target(...args);
} finally {
visiting.delete(typeName); // 清理访问记录
}
}
}4. 在 TypeScript/JavaScript 框架中的应用
NestJS 示例
ts
@Injectable()
export class EmailService {
send() { /* ... */ }
}
@Injectable()
export class UserService {
constructor(private emailService: EmailService) {} // 自动注入
}
// 控制器中使用
@Controller('users')
export class UsersController {
constructor(private userService: UserService) {} // 自动注入
}Angular 示例
ts
@Injectable({ providedIn: 'root' })
export class HttpService {
// ...
}
@Component({
selector: 'app-user',
template: '...'
})
export class UserComponent {
constructor(private httpService: HttpService) {} // 自动注入
}5. 依赖注入的核心优势
✅ 解耦
- 类只依赖抽象接口,不依赖具体实现
- 可以轻松替换依赖的实现
✅ 可维护性
- 修改依赖实现不影响使用方代码
- 符合开闭原则(对扩展开放,对修改关闭)
✅ 可测试性
ts
// 测试时可以轻松 mock 依赖
class MockNotificationService implements NotificationService {
sendCalls: string[] = [];
send(to: string, message: string) {
this.sendCalls.push(`${to}: ${message}`);
}
}
// 测试 UserService
const mockService = new MockNotificationService();
const userService = new UserService(mockService);
userService.register("test@example.com");
expect(mockService.sendCalls).toHaveLength(1);6. 实际应用场景
- Web 框架:处理 HTTP 请求、数据库连接、日志服务等
- 企业应用:复杂的业务逻辑层,需要灵活替换服务实现
- 微服务架构:不同服务间的依赖管理
- 插件系统:动态加载和注入插件依赖
总结
依赖注入的本质是将对象的创建和使用分离,通过外部注入依赖来实现松耦合。它不是必需的,但在构建大型、可维护、可测试的应用程序时,是一个非常有价值的模式。现代 TypeScript 框架(如 NestJS、Angular)都内置了强大的依赖注入系统,让开发者能够专注于业务逻辑而不是依赖管理。