2024年11月08日7 分钟

控制反转(IoC)与依赖注入(DI):从原理到前端实践

深入理解控制反转的设计思想,以及它在 React、Angular 和 Node.js 中的实际应用

什么是控制反转

控制反转(Inversion of Control,IoC)不是一种具体的技术,而是一种设计原则。它的核心思想是:

不要自己去找依赖,让依赖来找你。

// ❌ 传统方式:我主动创建依赖("我控制")
class UserService {
  constructor() {
    this.db = new MySQL({ host: 'localhost', port: 3306 })  // 自己创建
    this.cache = new Redis({ host: 'localhost', port: 6379 }) // 自己创建
    this.logger = new FileLogger('/var/log/app.log')          // 自己创建
  }
}

// ✅ 控制反转:依赖由外部注入("框架控制")
class UserService {
  constructor(db, cache, logger) {  // 外部传入
    this.db = db
    this.cache = cache
    this.logger = logger
  }
}

"控制"指的是创建和管理依赖的控制权。反转前,组件自己管理;反转后,交给外部(容器/框架)管理。

为什么需要控制反转

问题:高耦合

// ❌ UserService 直接依赖 MySQL
class UserService {
  constructor() {
    this.db = new MySQL()  // 硬编码依赖
  }

  async getUser(id) {
    return this.db.query('SELECT * FROM users WHERE id = ?', [id])
  }
}

// 问题1:想换成 PostgreSQL?→ 要改 UserService 的代码
// 问题2:想单元测试?→ 必须连真实数据库
// 问题3:想在不同环境用不同配置?→ 改代码

解决:依赖注入

// ✅ 定义接口(抽象)
interface Database {
  query(sql: string, params: any[]): Promise<any>
}

// ✅ UserService 依赖接口,不依赖具体实现
class UserService {
  constructor(private db: Database) {}  // 注入

  async getUser(id: string) {
    return this.db.query('SELECT * FROM users WHERE id = ?', [id])
  }
}

// 正式环境
const service = new UserService(new MySQL(prodConfig))

// 测试环境
const service = new UserService(new MockDB())

// 换数据库
const service = new UserService(new PostgreSQL(config))

IoC 的三种实现方式

1. 依赖注入(Dependency Injection)

最常见的 IoC 实现方式:

// 构造函数注入
class OrderService {
  constructor(
    private userService: UserService,
    private paymentService: PaymentService,
    private emailService: EmailService,
  ) {}
}

// 属性注入(装饰器方式,Angular/NestJS)
class OrderService {
  @Inject(UserService)
  private userService: UserService

  @Inject(PaymentService)
  private paymentService: PaymentService
}

// 接口注入(setter 方式)
class OrderService {
  setUserService(service: UserService) {
    this.userService = service
  }
}

2. 依赖查找(Dependency Lookup)

组件主动从容器中查找依赖:

class OrderService {
  private userService: UserService

  constructor(container: Container) {
    this.userService = container.get(UserService)  // 主动查找
  }
}

3. 模板方法模式

框架定义流程骨架,你填充具体步骤:

// React 就是控制反转的典型例子!
// React 控制了渲染流程,你只提供组件

function MyComponent() {
  // 你不需要自己调用 DOM API
  // React 控制什么时候渲染、怎么渲染
  return <div>Hello</div>
}

// 你没有调用 React,是 React 在调用你的组件
// 这就是"控制反转"

IoC 容器

IoC 容器是管理所有依赖关系的"大管家":

// 简单的 IoC 容器实现
class Container {
  private registry = new Map<string, { factory: Function, singleton: boolean }>()
  private instances = new Map<string, any>()

  // 注册服务
  register<T>(token: string, factory: () => T, singleton = true) {
    this.registry.set(token, { factory, singleton })
  }

  // 获取服务
  resolve<T>(token: string): T {
    const registration = this.registry.get(token)
    if (!registration) throw new Error(`未注册: ${token}`)

    // 单例模式:返回缓存实例
    if (registration.singleton) {
      if (!this.instances.has(token)) {
        this.instances.set(token, registration.factory())
      }
      return this.instances.get(token)
    }

    // 非单例:每次创建新实例
    return registration.factory()
  }
}

// 使用
const container = new Container()

container.register('Database', () => new MySQL(config))
container.register('Cache', () => new Redis(config))
container.register('UserService', () => new UserService(
  container.resolve('Database'),
  container.resolve('Cache'),
))

const userService = container.resolve<UserService>('UserService')

前端中的 IoC 实践

React Context = IoC 容器

// React 的 Context 就是一个轻量级 IoC 容器

// 1. 注册服务(Provider)
const ThemeContext = createContext<Theme>('light')
const AuthContext = createContext<AuthState | null>(null)

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <AuthContext.Provider value={{ user, login, logout }}>
        <Page />
      </AuthContext.Provider>
    </ThemeContext.Provider>
  )
}

// 2. 消费服务(注入)
function Button() {
  const theme = useContext(ThemeContext)  // 依赖注入!
  const auth = useContext(AuthContext)    // 依赖注入!
  // Button 不知道 theme 从哪来,也不关心
}

React 的 Hooks 就是控制反转

// 你不控制状态的存储方式
const [count, setCount] = useState(0)
// React 控制 state 存在哪里(Fiber 节点上)
// React 控制什么时候重新渲染
// React 控制批量更新的时机

// 你不控制副作用的执行时机
useEffect(() => {
  // 你只提供"做什么"
  fetchData()
}, [id])
// React 控制"什么时候做"(commit 之后、paint 之后)
// React 控制"是否要做"(依赖没变就跳过)

Angular 的完整 DI 系统

// Angular 有最完善的前端 DI 系统

@Injectable({ providedIn: 'root' })  // 注册到根容器
class UserService {
  constructor(
    private http: HttpClient,      // Angular 自动注入
    private auth: AuthService,     // Angular 自动注入
  ) {}
}

@Component({
  selector: 'app-user-list',
  template: `<div *ngFor="let u of users">{{u.name}}</div>`,
})
class UserListComponent {
  constructor(private userService: UserService) {}  // 自动注入
}

NestJS 的 DI 容器

// NestJS(Node.js 后端)完全基于 IoC/DI

@Injectable()
class UserService {
  constructor(
    @InjectRepository(User) private userRepo: Repository<User>,
    private cacheService: CacheService,
  ) {}

  async findAll() {
    const cached = await this.cacheService.get('users')
    if (cached) return cached
    return this.userRepo.find()
  }
}

@Controller('users')
class UserController {
  constructor(private userService: UserService) {}  // 自动注入

  @Get()
  findAll() {
    return this.userService.findAll()
  }
}

@Module({
  controllers: [UserController],
  providers: [UserService, CacheService],  // 注册到 IoC 容器
})
class UserModule {}

IoC 的核心好处

1. 解耦
   组件不再直接创建依赖 → 更容易替换、扩展

2. 可测试
   测试时注入 Mock 对象 → 不需要真实数据库/网络

3. 可配置
   不同环境注入不同实现 → 无需改代码

4. 关注点分离
   组件只关心"用什么" → 不关心"怎么创建"

5. 生命周期管理
   容器统一管理创建、缓存、销毁 → 避免内存泄漏

总结

控制反转不是某个具体的 API,而是贯穿整个软件设计的思想:

  • React 的整个组件模型就是控制反转(你写组件,React 控制渲染)
  • useContext 是 React 中的依赖注入机制
  • Angular/NestJS 提供了完整的 IoC 容器和装饰器语法
  • 核心原则:依赖抽象(接口),不依赖具体实现