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

Swift 如何将单例模式与依赖项注入结合使用?,swift,design-patterns,dependency-injection,singleton,Swift,Design Patterns,Dependency Injection,Singleton,我最近听说,使用依赖注入是“当今软件开发世界中唯一社会上可以接受的使用单例的方法”。我现在不一定要对这一说法的准确性进行辩论,因为它主要是基于观点的。我现在的目标是了解如何准确地将依赖项注入与单例模式结合使用 例如,在我最新的iOS应用程序中,我有一个服务层,在其中保存我的URLSession代码。我将此层创建为单体: struct ServiceSingleton { private init() static let shared = ServiceSingleton()

我最近听说,使用依赖注入是“当今软件开发世界中唯一社会上可以接受的使用单例的方法”。我现在不一定要对这一说法的准确性进行辩论,因为它主要是基于观点的。我现在的目标是了解如何准确地将依赖项注入与单例模式结合使用

例如,在我最新的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中添加如下内容:

TL;医生:

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

(2) 你能给我解释一下这是怎么实现的吗

(3) 从现在开始,当我在iOS项目中使用单例模式时,我应该一直使用DI吗

  • 您能告诉我如何在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()
    
    通常情况下,ViewController的初始化由情节串连板完成,因此您不能通过初始化器参数来确定依赖项,而必须使用对象初始化后设置的存储属性

  • 你能给我解释一下这是怎么实现的吗

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

    如果所有代码都直接使用产品依赖项,那么您将

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

  • 没有真正的“单元”测试。每个测试都将测试一个代码单元,以及它所依赖的所有公共依赖项。如果您对其中一个依赖项进行代码更改,它将破坏系统中的大多数单元测试,这使得很难准确确定失败的内容。通过解耦依赖关系,您可以使用模拟对象,这些对象执行支持单元测试所需的最低限度的操作,并确保每个测试只测试特定的代码单元,而不是它所依赖的可传递依赖关系

  • 从现在开始,当我在iOS项目中使用单例模式时,我应该一直使用DI吗

    养成这个好习惯。当然,也有一些质量低劣和肮脏的项目,你只是想快速推进,并不在意,但你会惊讶地发现,这些所谓的质量低劣和肮脏的项目中有多少真的成功了,并为此付出了代价。你只需要意识到什么时候你在妨碍自己,而不是花一些额外的时间去分离你的礼仪

  • 您能告诉我如何在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()
    
    通常情况下,ViewController的初始化由情节串连板完成,因此您不能通过初始化器参数来确定依赖项,而必须使用对象初始化后设置的存储属性

  • 你能给我解释一下这是怎么实现的吗

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

    如果所有代码都直接使用产品依赖项,那么您将

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

  • 没有真正的“单元”测试。每个测试都将测试一个代码单元,以及它所依赖的所有公共依赖项。如果您对其中一个依赖项进行代码更改,它将破坏系统中的大多数单元测试,这使得很难准确确定失败的内容。通过解耦依赖关系,您可以使用模拟对象,这些对象执行支持单元测试所需的最低限度的操作,并确保每个测试只测试特定的代码单元,而不是它所依赖的可传递依赖关系

  • 从现在开始,当我在iOS项目中使用单例模式时,我应该一直使用DI吗

    养成这个好习惯。当然,也有一些质量低劣和肮脏的项目,你只是想快速推进,并不在意,但你会惊讶地发现,这些所谓的质量低劣和肮脏的项目中有多少真的成功了,并为此付出了代价。你只需要意识到什么时候你在妨碍自己,而不是花一些额外的时间去分离你的礼仪


  • 当然可以你能开个聊天室和我聊天吗?我现在有空,但我要走了soon@AlwaysLearning好的,等你有机会的时候就开始聊天室,提前输入你的问题,提到我的用户名,等我有空的时候我会及时回复。库尔