Skip to content

控制反转

控制反转的英文是 Inversion of Control,简称 IoC。它不是某个具体 API,也不等同于依赖注入。IoC 描述的是一种架构关系:原本由业务代码主动掌握的部分控制权,被交给框架、容器或运行时;业务代码转而提供可被调用的组件与规则。

先不要记住 IoC

先看一个纯手工程序。假设我们要启动一个聊天机器人:

kotlin
fun main() {
    val config = loadConfig()
    val database = Database(config.databaseUrl)
    val userService = UserService(database)
    val messageHandler = MessageHandler(userService)

    database.connect()
    messageHandler.register()
    waitForShutdown()
    database.close()
}

请先停下来观察:main 做了多少种不同的事?

  • 它读取配置;
  • 它知道每个类的构造方式;
  • 它知道对象之间的依赖关系;
  • 它知道谁先启动;
  • 它知道谁要注册;
  • 它知道关闭时要释放什么。

这段代码仍然可以工作。但随着模块增加,main 会逐渐变成“知道所有事情的地方”。真正推动框架出现的,不是开发者不愿意写 new,而是这些组装规则越来越多,并且每个项目都在重复

停下来想一想:如果再加入任务调度和事件监听,会发生什么?

你可能需要继续在 main 中创建调度器、寻找任务、注册监听器、处理启动失败和关闭顺序。业务模块每增加一种能力,中央入口都要知道更多细节,模块很难独立接入。

当我们发现“很多应用都需要相似的启动、扫描、组装和关闭流程”时,就会自然想到:能否把这套稳定流程写一次,由各应用只提供不同的组件?这一步思考,才是理解 IoC 的起点。

从手工组装开始

kotlin
fun main() {
    val database = Database("jdbc:example")
    val repository = UserRepository(database)
    val auditService = AuditService(SystemClock())
    val userService = UserService(repository, auditService)

    userService.start()
}

对于规模很小、依赖关系稳定的程序,这种写法清晰而可靠。问题通常在应用增长后出现:

  • 同一个对象被多个模块共享时,谁保存它?
  • 实现替换后,要修改多少处构造代码?
  • 接口存在多个实现时,由谁选择?
  • 对象创建后还要注入字段、读取配置和注册监听器,谁负责?
  • 服务应以什么顺序启动和停止?
  • 插件在编译时不知道全部业务类型,如何参与组装?

当“创建对象”扩展为发现、选择、创建、配置、注入、初始化、注册和销毁的一整套流程时,统一的对象管理机制就有了价值。

对象创建权的反转

传统代码主动创建依赖:

kotlin
val service = UserService(UserRepository(Database()))

使用 Rain 后,对象只声明需要什么:

kotlin
class UserService(
    private val repository: UserRepository,
)

谁创建 UserService、什么时候创建、UserRepository 从哪里来,改由 Rain 的 DiContextClassContextBeanCreator 协作决定。

反转之后,业务类不再承担整个依赖图的组装责任。它只负责自己的业务行为和直接依赖。

程序调用方向的反转

框架式程序往往先把组件交给框架,等待框架在合适时间调用:

kotlin
class CacheService : ApplicationService {
    override fun start() {
        // 由 Rain 在应用服务启动阶段调用
    }

    override fun stop() {
        // 由 Rain 在应用停止阶段调用
    }
}

事件监听器也是相同道理:

kotlin
@EventListener
class UserListener {
    @SubscribeEvent
    fun onUserCreated(event: UserCreated) {
        // 由 EventBus 调用
    }
}

业务代码仍然会主动调用其他对象,但总体流程、调用时机和基础设施边界由框架掌握。

扩展发现权的反转

Rain 的 AppLoader 扫描 rain.scanPackages,发现具体类、接口实现、自定义注解、LoaderApplicationService、事件监听器和任务。

业务模块不必把每个扩展逐项写入中央入口,只需满足框架约定:

kotlin
@LoadBy(MyFeatureLoader::class)
annotation class MyFeature

框架扫描到 @MyFeature 后,将对应类型整理为 LoadItem 并交给 MyFeatureLoader。应用负责声明,框架负责发现和调度。

生命周期控制权的反转

Application.start() 会初始化配置、创建 DI 上下文、扫描应用、执行 Loader、启动 ApplicationService 并发布应用状态事件。JVM 关闭时,Rain 再驱动停止流程。

对象不仅由框架创建,也可能由框架决定何时进入可服务状态、何时注册到基础设施、何时释放资源。

Rain 中的 IoC 范围

在 Rain 中,控制反转至少涉及:

  1. 对象由谁创建;
  2. 依赖由谁解析;
  3. 扩展由谁发现;
  4. 生命周期由谁驱动;
  5. 事件、任务和 Controller 由谁调用;
  6. 类在加载时是否以及如何被增强。

因此,“Rain 是 IoC 框架”不能只理解为“构造器参数会自动传入”。DI 是其中重要的一部分,但不是全部。

IoC 不意味着消灭显式代码

控制权交给框架后,行为会变得更声明式,也更隐式。正确的目标不是让业务代码完全看不到流程,而是把稳定、重复的机制交给框架,把真正变化的业务策略保留在应用中。

如果一个流程只在单一位置发生、步骤简单且不需要扩展,显式调用往往比新增注解和扫描机制更清楚。IoC 应用于“值得统一控制的变化点”,而不是所有代码。

用 Rain 重新观察启动过程

Rain 中的业务入口可能只有:

kotlin
fun main() {
    FullStackApplicationLauncher.launch()
}

这并不代表工作消失了。工作只是移动到了框架中:

text
Application
├─ 初始化 ConfigImpl
├─ 初始化 ContextImpl
├─ 让 AppLoader 扫描类
├─ 建立 Bean 与接口绑定
├─ 执行各模块 Loader
├─ 启动 ApplicationService
└─ 注册关闭流程

业务代码少了,是因为 Rain 接管了稳定流程;业务模块仍然要提供配置、组件、监听器、任务和服务实现。

新人自测

尝试不用术语回答下面的问题:

  1. Rain 启动后,为什么会自动发现 @EventListener
  2. 为什么一个 ApplicationService 不需要自己调用 start()
  3. 如果没有 Rain,这些发现和调用代码最可能写在哪里?
  4. 把流程交给框架后,我们得到什么,又失去什么?

如果你的回答接近“框架统一掌握发现、组装和调用时机,但程序行为变得更依赖约定”,你已经理解了 IoC 的核心。

基于 Apache License 2.0 发布