模式:单例与静态变量和方法方法

时间:2018-10-18 17:36:12

标签: swift design-patterns architecture static singleton

我正在阅读有关Singleton模式的很多文章。我目前正在使用它在我的第一个应用程序中存储全局状态组。我到达了一个不知道要实现API客户端类的方法的地步。

具有静态变量静态函数的结构是否存在相同的问题?

为了说明我的意思,我尝试两次编写相同的高度简化且完全相同的方案。

1。视图控制器正在处理一个单例:

struct APIClientSingletonClass {

    static let shared = APIClientSingletonClass()

    var stateOfSomehting: Bool = true
    var stateOfSomehtingMore: Bool = false
    var stateNumber: CGFloat = 1234
    var stateOfSomehtingComputed: CGFloat {
        return stateNumber * 10
    }

    func convertSomethingToSomethingElse() {
        // calling method in self like this:
        otherMethod()
    }
    func otherMethod() {
        // doing stuff here
    }
}



// Calling APIClient from outside:
class ViewControllerTalkingToSingleton: UIViewController {

    var api = APIClientSingletonClass.shared

    override func viewDidLoad() {
        super.viewDidLoad()
        api.convertSomethingToSomethingElse()
        api.stateOfSomehting = false
    }
}

2。另一种方法:

struct APIClientStruct {

    static var stateOfSomehting: Bool = true
    static var stateOfSomehtingMore: Bool = false
    static var stateNumber: CGFloat = 1234
    static var stateOfSomehtingComputed: CGFloat {
        return stateNumber * 10
    }

    static func convertSomethingToSomethingElse() {
        // calling method in self like this:
        APIClientStruct.otherMethod()
    }

    static func otherMethod() {
        // doing stuff here
    }
}


// Calling APIClient from outside:
class ViewControllerTalkingToStruct: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        APIClientStruct.convertSomethingToSomethingElse()
        APIClientStruct.stateOfSomehting = false
    }
}

你们怎么看?方法2是否落入似乎使Singletons成为双刃剑的陷阱中?

任何输入都非常感谢! 最好的柏林

编辑: 这个线程非常有趣,但是我不确定它是否确实与我的问题有关:

Difference between static class and singleton pattern?

由于对此主题有很多观点,所以让我指定: 我的方法2在测试和代码可维护性方面是否具有相同的问题含义?

3 个答案:

答案 0 :(得分:2)

基于类的单例是可行的方法,只要您为测试提供依赖注入。实现此目的的方法是为您的应用程序创建一个单例,称为DependencyManager。在您的AppDelegate(或其他类,如果需要)中,您可以创建要挂在DependencyManager上的任何控制器,网络服务,领域模型等,然后将它们分配给DependencyManager。您的单元测试将跳过此代码。

然后,您的单元测试可以访问DependencyManager(并在首次访问时实例化DependencyManager),并以每个单元测试所需的程度填充这些控制器和服务的模拟版本。

您的UIViewController,您的MVVM视图模型等可以单例访问DependencyManager,从而获得真实的控制器和服务,或者它们的模拟版本,具体取决于您是否正在运行应用程序或单元测试。

如果您正在执行MVVM,我还建议当UIViewController要创建其视图模型类时,它首先检查DependencyManager中的特殊属性以查看是否存在嘲笑模型。单个属性可以满足此目的,因为一次只能测试您的UIViewController之一。它会使用该属性,而不是为其自身创建新的视图模型。这样,您可以在测试每个UIViewController时模拟视图模型。 (还有其他一些技巧可以支持一个UIViewController进行测试,但这里不再赘述。)

请注意,以上所有内容均可与还希望使用情节提要和/或笔尖的应用很好地配合使用。人们之所以对情节提要感到沮丧,是因为他们不知道如何为他们的视图控制器进行模拟服务的依赖注入。好了,以上就是解决方案!只需确保在设置DependencyManager之后在AppDelegate中加载情节提要。 (从您的info.plist中删除情节提要名称,然后在AppDelegate中自己实例化此情节提要。)

我已经以此方式编写了一些附带的应用程序,以及一些SDK的示例应用程序以及测试。我强烈推荐这种方法!并且一定要在每个此类的开发期间或开发之后立即编写单元测试和viewController测试,否则您将永远无法解决它们!

答案 1 :(得分:1)

我将使用基于类的Singleton。只要记住有一个单身人士的两个标准即可。您需要程序中的 GLOBAL ACCESS SINGLE INSTANCE 。基于结构的单例将失败有几个问题。为新变量分配结构后,Swift会在后台进行完整复制。

使用此链接可以找到另一个有用的信息片段。

What's the difference between Struct based and Class based singletons?

答案 2 :(得分:1)

通常使单身对象难以测试的是通常总是直接访问单例对象。因此,您没有办法用模拟对象代替实际的单例对象(例如,由数据库支持的数据存储)进行测试(例如,由易于配置的数组支持的数据存储)预定义的测试数据)。

使用静态成员具有相同的基本问题。直接引用静态成员时,您没有替代真实生产实现的模拟对象的方法。

解决方案非常简单:不要直接访问单例成员。我所做的就是这样:

// An example of a dependency.
protocol DataAccessLayer {
    func getData() -> [Int]
}

// A real implementation of DataAccessLayer, backed by a real production database
class ProdDB: DataAccessLayer {
    static let instance = ProdDB()
    private init() {}

    func getData() -> [Int] {
        return [1, 2, 3] // pretend this actually queries a DB
    }
}

// A mcok implementation of DataAccessLayer, made for simple testing using mock data, without involving a production database.
class MockDB: DataAccessLayer {
    func getData() -> [Int] {
        return [1, 2, 3] // The mock *actually* hardcodes this data
    }
}


// A protocol that stores all databases and services used throughout your app
protocol ServiceContextProtocol {
    var dataAccessLayer: DataAccessLayer { get } // Present via protocol, either real impl or mock can go here
    //var fooAPIGateway: FooAPIGateway { get }
    //... add all other databases and services here
}

// The real service context, containing real databases and service gateways
class ProdServiceContext: ServiceContextProtocol {
    let dataAccessLayer: DataAccessLayer = ProdDB.instance
    //var fooAPIGateway: ProdFooAPIGateway { get }
    //... add all other prod databases and services here
}

// A mock service context, used in testing, which provides mocked databases and service gatways
class MockServiceContext: ServiceContextProtocol {
    let dataAccessLayer: DataAccessLayer  = MockDB()
    //var fooAPIGateway: MockFooAPIGateway { get }
    //... add all other mock databases and services here
}

let debug = false // Set this true when you're running in a test context

// A global variable through which you access all other global state (databases, services, etc.)
let ServiceContext: ServiceContextProtocol = debug ? MockServiceContext() : ProdServiceContext()

// Always reference ServiceContext.dataAccessLayer, ServiceContext.fooAPIGateway, etc.
// and *never* reference ProdDB.instance of MockDB directly.