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

Alw*_*ing 1 singleton design-patterns dependency-injection swift

我最近听说使用依赖注入是“当今软件开发世界中使用单例的唯一社会可接受的方式”。我现在不一定想辩论这个陈述的准确性,因为它主要是基于意见的。我现在的目标是了解如何在单例模式中使用依赖注入。

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

struct ServiceSingleton {

    private init()

    static let shared = ServiceSingleton()

    func fetchJSON() {
     // URLSession code
    }

}
Run Code Online (Sandbox Code Playgroud)

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

class ViewController: UIViewController() {

    override viewDidLoad() {
        super.viewDidLoad()

        fetchData()    

    }

    fileprivate func fetchData() {

        ServiceSingleton.shared.fetchJSON()
    }

}
Run Code Online (Sandbox Code Playgroud)

当然,上面的代码使用了单例,但是没有使用依赖注入。我知道,如果我想一般使用依赖注入,我会向ViewController添加这样的东西:

// Dependency Injection Constructor
override init(someProperty: SomePropertyType) {
    self.someProperty = someProperty
    super.init()
}
Run Code Online (Sandbox Code Playgroud)

特尔;博士:

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

(2) 你能向我解释一下这有什么作用吗?

(3) 以后在我的iOS项目中使用单例模式时,是否应该一直使用DI?

Ale*_*ica 9

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

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

    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()
    
    Run Code Online (Sandbox Code Playgroud)

    通常 ViewController 的初始化是由你的故事板完成的,所以你不能通过初始化参数来获取你的依赖项,而必须使用在对象初始化后设置的存储属性。

  2. 你能向我解释一下这有什么作用吗?

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

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

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

    2. 没有真正的“单元”测试。每个测试都将测试一个代码单元,以及它传递依赖的所有常见依赖项。如果您要对这些依赖项之一进行代码更改,则会破坏系统中的大多数单元测试,这使得确定失败的确切原因变得更加困难。通过解耦您的依赖关系,您可以使用模拟对象来支持单元测试所需的最低限度,并确保每个测试只测试特定的代码单元,而不是它所依赖的传递依赖关系。

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

    捡起来是个好习惯。当然,有一些 qucik-and-dirty-projects 你只想快速行动而不会真正关心,但你会惊讶这些所谓的 qucik-and-dirty-projects 中有多少实际上起飞了,并且支付路上的费用。你只需要意识到什么时候你会因为没有花一些额外的时间来解耦你的体面而阻碍了自己。