如何结合依赖注入使用单例模式?

How to use the singleton pattern in conjunction with dependency injection?

最近听说使用依赖注入是"the only socially acceptable way to use a singleton in today's software development world"。我现在不一定想争论这个声明的准确性,因为它主要是基于意见的。我现在的目标是了解如何我可以使用单例模式的依赖注入。

例如,在我最新的 iOS 应用程序中,我有一个服务层,用于保存我的 URLSession 代码。我将这一层创建为单例:

struct ServiceSingleton {

    private init()

    static let shared = ServiceSingleton()

    func fetchJSON() {
     // URLSession code
    }

}

然后我在ViewController中使用shared,如下:

class ViewController: UIViewController() {

    override viewDidLoad() {
        super.viewDidLoad()

        fetchData()    

    }

    fileprivate func fetchData() {

        ServiceSingleton.shared.fetchJSON()
    }

}

当然,上面的代码使用了单例,但是没有使用依赖注入。我知道如果我想在一般情况下使用依赖注入,我会在 ViewController:

中添加这样的内容
// Dependency Injection Constructor
override init(someProperty: SomePropertyType) {
    self.someProperty = someProperty
    super.init()
}

TL;DR:

(1) 你能告诉我如何在 Swift 中正确使用单例模式的依赖注入吗?

(2) 你能给我解释一下这实现了什么吗?

(3) 从现在开始,当我在我的 iOS 项目中使用单例模式时,我是否应该 总是 使用 DI?

  1. Could you show me how to properly use dependency injection with the singleton pattern in Swift?

    不是直接访问 ServiceSingleton.shared,而是访问注入到对象中的实例变量,如果可能,通常在初始化程序中,否则作为可设置的 属性、post-初始化:

    protocol FooService {
        func doFooStuff()
    }
    
    class ProductionFooService: FooService {
    
        private init() {}
    
        static let shared = ProductionFooService()
    
        func doFooStuff() {
            print("real URLSession code goes here")
        }
    
    }
    
    struct MockFooService: FooService {
        func doFooStuff() {
            print("Doing fake foo stuff!")
        }
    }
    
    class FooUser {
        let fooService: FooService
    
        init(fooService: FooService) { // "initializer based" injection
            self.fooService = fooService
        }
    
        func useFoo() {
            fooService.doFooStuff() // Doesn't directly call ProductionFooService.shared.doFooStuff
        }
    }
    
    let isRunningInAUnitTest = false
    
    let fooUser: FooUser
    if !isRunningInAUnitTest {
        fooUser = FooUser(fooService: ProductionFooService.shared) // In a release build, this is used.
    }
    else {
        fooUser = FooUser(fooService: MockFooService()) // In a unit test, this is used. 
    }
    
    fooUser.useFoo()
    

    通常情况下,ViewController 的初始化是由您的故事板完成的,因此您不能通过初始化参数引入依赖项,而必须改为使用对象初始化后设置的存储属性。

  2. Could you explain to me what this achieves?

    您的代码不再耦合到 ProductionFooService.shared。因此,您可以引入 FooService 的不同实现,例如用于 beta 环境的实现,用于单元测试的模拟实现等。

    如果您的所有代码普遍直接使用您的产品依赖项,您将...

    1. 发现不可能在测试环境中实例化您的对象。您不希望您的单元测试、CI 测试环境、测试版环境等连接到产品数据库、服务和 API。

    2. 没有真正的 "unit" 测试。每个测试都将测试一个代码单元,以及它传递依赖的所有常见依赖项。如果您曾经对这些依赖项之一进行代码更改,它将破坏系统中的大部分单元测试,这使得更难准确确定失败的原因。通过解耦您的依赖关系,您可以使用模拟对象来执行支持单元测试所需的最低限度,并确保每个测试仅测试特定的代码单元,而不是它所依赖的传递依赖关系。

  3. Should I always use DI when I use the singleton pattern in my iOS projects from now on?

    捡起来是个好习惯。当然,有些快速而肮脏的项目你只是想快速行动而不会真正关心,但你会惊讶于这些所谓的快速和肮脏的项目中有多少真正起飞,并且付出代价。你只需要意识到什么时候你因为没有花一些额外的时间来消除你的体面而阻碍了自己。