在Swift中通过依赖注入创建全局单例是否属于不良实践?
问题背景
我们先定义如下的User类:
class User { var id: Int var name: String var phone: String }
接着是用于获取用户信息的UserService协议:
protocol UserService { func getUser(withID id: Int, completion: @escaping(_ user: User, _ error: Error) -> Void) }
对应的API实现类UserAPIService和测试实现类UserTestService:
class UserAPIService: UserService { func getUser(withID id: Int, completion: @escaping(_ user: User, _ error: Error) -> Void) { // 从API获取用户数据 } }
class UserTestService: UserService { func getUser(withID id: Int, completion: @escaping(_ user: User, _ error: Error) -> Void) { // 返回测试用用户数据 } }
常规做法是在需要该服务的类中注入对应实例(生产环境用UserAPIService,测试环境用UserTestService),但每个ViewModel都注入会比较繁琐。于是考虑在应用/测试启动时创建UserService单例,通过UserServiceManager管理:
fileprivate var _current: UserService? class UserServiceManager { static var current: UserService { get { if let c = _current { return c } return UserAPIService() // 如果未设置则返回默认实现 } set { _current = newValue } } }
在AppDelegate的启动方法或测试的setUpWithError中设置实例:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { UserServiceManager.current = UserAPIService() return true }
override func setUpWithError() throws { UserServiceManager.current = UserTestService() }
这样无需到处注入,直接使用UserServiceManager.current即可。当前的疑问是:这种方式是否属于不良实践?如果是,原因是什么?目前仅顾虑拆分模块时需在AppDelegate中导入各模块。
回答
这种方式属于服务定位器模式,是依赖注入的一种变体,本身不算绝对的不良实践,但存在一些潜在问题,需要结合项目规模和团队情况权衡:
潜在问题
- 隐式依赖:使用
UserServiceManager.current的类,其依赖关系不会在初始化方法或属性中显式体现。新开发者需要查看内部代码才能发现该类依赖了UserService,增加了理解成本,也容易导致依赖关系混乱。 - 测试隔离性风险:虽然能在测试前替换服务实例,但如果某个测试需要特殊的
UserService实现(比如模拟特定错误场景),全局替换可能会影响其他测试的独立性,需要额外注意测试执行顺序或在每个测试前后重置实例。 - 模块耦合加剧:正如你顾虑的,项目拆分模块后,
AppDelegate需要导入各个模块的服务实现类,导致模块间耦合度升高,违反了依赖倒置原则(上层模块不应直接依赖下层模块的具体实现)。 - 资源生命周期问题:全局单例一旦创建会常驻内存,如果
UserService持有网络连接、缓存等资源,可能导致资源无法及时释放,甚至引发内存泄漏。
适用场景
如果项目规模较小、团队成员熟悉这种模式,且短期内没有大规模模块拆分计划,这种方式可以减少注入代码的繁琐性,提升开发效率。但如果项目会逐步扩张,建议优先选择构造函数注入或属性注入,配合依赖注入框架(如Swinject)管理依赖,这样能更好地保持代码的可维护性和可测试性。
内容的提问来源于stack exchange,提问作者Sam
相关产品推荐
相关产品推荐

