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

在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 15:24:20