You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实现库类单例兼顾内部访问与测试替换的通用模式是什么?

可测试的库级单例实现方案

你描述的场景不需要硬套复杂模式,用「接口抽象+可配置服务注册表」的轻量实现就能完全覆盖需求,既避免硬编码MyService.getInstance()带来的强耦合问题,也不需要在所有类层级层层传递实例,同时完美支持测试替身替换。

具体实现逻辑

  • 首先抽离MyService的公开接口,所有库内部逻辑、客户端调用都只依赖接口,不耦合具体实现类:
// 对外公开的服务接口
interface MyService {
    fun executeTask()
}

// 库内部的真实服务实现,构造方法不对外暴露
internal class RealMyService(private val config: ServiceConfig) : MyService {
    override fun executeTask() {
        // 真实业务逻辑
    }
}
  • 实现库内部统一的服务注册表作为实例访问入口,收口所有实例的存储、初始化、替换逻辑:
object LibServiceRegistry {
    @Volatile
    private var myServiceInstance: MyService? = null

    // 库内部代码统一通过这个方法获取服务实例
    fun getMyService(): MyService {
        return myServiceInstance ?: throw IllegalStateException(
            "MyService 未初始化,请先在客户端启动阶段调用初始化方法"
        )
    }

    // 给客户端暴露的初始化方法,仅允许调用一次,全局生成唯一实例
    fun initMyService(config: ServiceConfig): MyService {
        check(myServiceInstance == null) { "MyService 已完成初始化,不可重复创建" }
        return RealMyService(config).also { myServiceInstance = it }
    }

    // 仅用于单元测试的实例替换方法,生产代码无权限调用
    @VisibleForTesting
    fun setTestInstance(mockService: MyService?) {
        myServiceInstance = mockService
    }
}
  • 库内部类的依赖处理按需选择即可:
    • 核心逻辑类、对单测隔离要求高的类,保留构造函数注入实例的写法,单测时可以直接传入mock,不需要依赖全局注册表
    • 上层工具类、封装类可以直接通过LibServiceRegistry.getMyService()获取实例,减少层层传参的样板代码
  • 客户端使用成本极低,只需要在应用启动阶段调用一次初始化方法,后续所有位置拿到的都是同一个单例实例:
// 客户端应用启动入口
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        val config = loadServiceConfig()
        val myService = LibServiceRegistry.initMyService(config)
        // 客户端自身业务也可以直接使用该单例
        myService.executeTask()
    }
}

测试场景适配

单元测试不需要走正式初始化流程,直接调用测试专用方法注入mock/fake实例即可,测试完成后重置实例避免用例间状态污染:

@Test
fun testServiceDependentLogic() {
    // 注入测试替身
    val fakeService = FakeMyService()
    LibServiceRegistry.setTestInstance(fakeService)

    // 执行测试逻辑,所有库内部代码拿到的都是fake实例
    val testClass = MyTestableClass(LibServiceRegistry.getMyService())
    testClass.runAction()
    // 断言逻辑...

    // 测试完成后清空状态
    LibServiceRegistry.setTestInstance(null)
}

如果项目本身已经接入依赖注入框架(Koin/Hilt/Dagger等),可以直接用框架的单例绑定能力实现相同逻辑:生产环境将真实实现绑定为单例注入到所有依赖位置,测试环境替换绑定为测试替身即可,本质和上述轻量实现的思路一致,只是把注册表逻辑交给框架处理。

内容的提问来源于stack exchange,提问作者SilentByte

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 15:48:23