实现库类单例兼顾内部访问与测试替换的通用模式是什么?
可测试的库级单例实现方案
你描述的场景不需要硬套复杂模式,用「接口抽象+可配置服务注册表」的轻量实现就能完全覆盖需求,既避免硬编码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
相关产品推荐
相关产品推荐

