3. 服务定位器模式:Android中传统ServiceLocator的实现与局限性

依赖注入这事儿,说起来简单,做起来坑不少。很多团队一开始图省事,直接上服务定位器模式。嗯,我也干过这事儿。今天咱们就聊聊这个模式在Android里的实现,以及它到底有哪些坑。

3.1 什么是服务定位器模式?

说白了,服务定位器就是一个全局的“服务管家”。你告诉它你需要什么,它给你拿什么。它维护着一个服务注册表,所有依赖都集中管理。

我刚开始接触Android时,觉得这玩意儿真方便。不用搞什么Dagger、Koin,一个单例搞定所有依赖。但后来项目大了,才发现事情没那么简单。

3.2 一个典型的ServiceLocator实现

先看代码,咱们从最简单的开始:

object ServiceLocator {

    private val services = mutableMapOf<Class<*>, Any>()

    fun <T> register(serviceClass: Class<T>, service: T) {
        services[serviceClass] = service
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> resolve(serviceClass: Class<T>): T {
        return services[serviceClass] as? T
            ?: throw IllegalStateException("Service ${serviceClass.simpleName} not registered")
    }

    fun clear() {
        services.clear()
    }
}

使用起来也很直观:

// 初始化阶段
val apiService = ApiService()
val database = AppDatabase()
ServiceLocator.register(ApiService::class.java, apiService)
ServiceLocator.register(AppDatabase::class.java, database)

// 使用阶段
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val apiService = ServiceLocator.resolve(ApiService::class.java)
        // 使用apiService
    }
}

看着挺清爽对吧?但问题就藏在这清爽背后。

3.3 进阶实现:带作用域的ServiceLocator

后来我发现,有些服务需要跟着Activity生命周期走。于是我又加了一层作用域管理:

class ScopedServiceLocator(private val parent: ServiceLocator? = null) {

    private val services = mutableMapOf<Class<*>, Any>()

    fun <T> register(serviceClass: Class<T>, service: T) {
        services[serviceClass] = service
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> resolve(serviceClass: Class<T>): T {
        return services[serviceClass] as? T
            ?: parent?.resolve(serviceClass)
            ?: throw IllegalStateException("Service ${serviceClass.simpleName} not found")
    }

    fun clear() {
        services.clear()
    }
}

这样就能实现Activity级别的服务隔离了。但说实话,这代码写起来越来越像在造轮子。

3.4 服务定位器的局限性

我在项目中踩过的坑,基本都集中在下面这几点:

问题 具体表现 影响程度
运行时错误 服务未注册时,运行时才抛异常
依赖关系不透明 无法从代码中直接看出依赖关系
测试困难 全局状态导致测试间相互影响
生命周期管理 容易造成内存泄漏
类型安全 强制类型转换存在风险
⚠️ 我曾经踩过的坑: 有一次线上版本,因为某个服务在特定场景下没注册,导致整个模块崩溃。排查了半天才发现是初始化顺序的问题。从那以后,我对ServiceLocator就格外谨慎了。

3.5 为什么说它不适合大型项目?

你想想看,一个大型项目可能有几十个模块、上百个服务。用ServiceLocator管理,会面临几个致命问题:

  • 依赖关系不可见:代码里到处都是ServiceLocator.resolve(),你根本不知道一个类到底依赖了哪些服务
  • 初始化顺序依赖:服务A依赖服务B,服务B又依赖服务C,你得手动保证注册顺序
  • 全局状态污染:单例模式让所有测试用例共享状态,写单元测试时简直想哭
  • 重构困难:想改个服务的实现,得全局搜索ServiceLocator,一个个替换
核心问题: ServiceLocator本质上是一个服务定位器,而不是依赖注入容器。它把依赖的查找推迟到了运行时,失去了编译时的类型安全和依赖可见性。

3.6 什么时候可以用?

虽然我吐槽了这么多,但ServiceLocator也不是一无是处。我个人觉得,在以下场景还是可以考虑的:

  • 小型项目:团队小、模块少,用起来确实省事
  • 遗留系统改造:从传统模式向DI过渡时,作为中间方案
  • 特定场景:比如日志、配置等全局基础设施服务
💡 我的建议: 如果你决定用ServiceLocator,一定要做好以下几点:
  1. 在应用启动时完成所有服务的注册
  2. 使用接口而非具体类作为key
  3. 提供清晰的文档说明依赖关系
  4. 考虑使用Kotlin的委托属性简化调用

3.7 从ServiceLocator到真正的DI

说实话,ServiceLocator只是依赖注入的“入门版”。它解决了“怎么拿到依赖”的问题,但没解决“依赖怎么管理”的问题。

我后来转向了Koin和Dagger Hilt,才真正体会到什么是“依赖注入”。它们提供了:

  • 编译时的依赖检查
  • 自动的作用域管理
  • 清晰的依赖图
  • 更好的测试支持

嗯,下一章咱们就聊聊Koin这个轻量级的DI框架。它用Kotlin的DSL实现了依赖注入,用起来比ServiceLocator舒服多了。

记住一句话:ServiceLocator是“找服务”,真正的DI是“给服务”。这个思维转变,是理解依赖注入的关键。