什么时候不应该使用单例模式? (除了显而易见的)

Posted

技术标签:

【中文标题】什么时候不应该使用单例模式? (除了显而易见的)【英文标题】:When should the Singleton pattern NOT be used? (Besides the obvious) 【发布时间】:2011-05-03 17:09:53 【问题描述】:

我很清楚您想使用 Singleton 来提供对某些状态或服务的全局访问点。单例模式的好处就不用在这个问题中列举了。

我感兴趣的是,Singleton 一开始似乎是一个不错的选择,但可能会反过来咬你。一次又一次,我在 SO 的书籍和海报中看到作者说单例模式通常是一个非常糟糕的主意。

The Gang of Four 指出,在以下情况下您会想使用 Singleton:

一个类必须只有一个实例,并且客户端必须可以从众所周知的访问点访问它。 当唯一的实例应该可以通过子类化进行扩展,并且客户端应该能够在不修改其代码的情况下使用扩展的实例。

这些点虽然值得注意,但并不是我所寻求的实用点。

是否有人有一套规则或注意事项可用于评估您是否真的、真的确定要使用 Singleton?

【问题讨论】:

方法错误。你应该问“什么时候应该使用这种模式?”然后你会发现:never。单例是最糟糕的“模式”。 这提出了一个问题:什么情况是“显而易见的”?你应该知道没有什么是显而易见的。 谁对所有这些好答案投了反对票?让他们来。 也许有人太全神贯注于 SO 的投票系统... 【参考方案1】:

总结版本:

您知道您使用全局变量的频率吗?好的,现在使用 Singletons 甚至更少。事实上要少得多。几乎从不。它们共享全局变量在隐藏耦合方面的所有问题(直接影响可测试性和可维护性),并且通常“只能存在一个”限制实际上是一个错误的假设。

详细回答:

关于单例,最重要的一点是它是全局状态。这是一种暴露全局无限制访问的单个实例的模式。这具有全局变量在编程中的所有问题,但也采用了一些有趣的新实现细节,否则几乎没有真正的价值(或者,实际上,单实例方面可能会带来不必要的额外成本)。实现方式非常不同,以至于人们经常将其误认为是面向对象的封装方法,而实际上它只是一个花哨的全局单实例。

您应该考虑使用单例的唯一情况是,当拥有多个已全局数据的实例实际上是逻辑或硬件访问错误时。即使这样,您通常也不应该直接处理单例,而是提供一个包装器接口,is 允许您根据需要多次实例化,但只能访问全局状态。通过这种方式,您可以继续使用 dependency injection,并且如果您可以将全局状态与类的行为分开,那么这对您的系统来说并不是一个彻底的变化。

但是,当您似乎不依赖全局数据时,这会产生一些微妙的问题,但实际上确实如此。因此(使用包装单例的接口的dependency injection)只是一个建议而不是规则。一般来说,它仍然更好,因为至少您可以看到该类依赖于单例,而仅在类成员函数的内部使用 ::instance() 函数隐藏了该依赖关系。它还允许您提取依赖于全局状态的类并为它们制作更好的 unit tests,如果您将对单例的依赖直接烘焙到类中,您可以传入模拟无操作对象这要困难得多。

当烘焙一个单例 ::instance 调用时,它也将自己实例化为一个类,你使 不可能继承。变通方法通常会破坏单例的“单实例”部分。考虑这样一种情况,您有多个项目依赖于 NetworkManager 类中的共享代码。即使您希望此 NetworkManager 成为全局状态和单实例,您也应该非常怀疑将其变成单例。通过创建一个实例化自身的简单单例,您基本上使任何其他项目都无法从该类派生。

许多人认为ServiceLocator 是一种反模式,但我相信它比单例模式好半步,并且有效地掩盖了 Go4 模式的目的。实现服务定位器的方法有很多,但基本概念是将对象的构建和对象的访问分为两个步骤。这样,在运行时,您可以连接适当的派生服务,然后从单个全局联系点访问它。这具有显式对象构造顺序的好处,并且还允许您从基础对象派生。出于上述大多数原因,这仍然很糟糕,但它没有比 Singleton 糟糕,并且可以直接替代。

可接受的单例(阅读:服务定位器)的一个具体示例可能是包装像 SDL_mixer 这样的单实例 c 样式接口。单例的一个示例通常天真地实现它可能不应该是在日志记录类中(当您想记录到控制台和磁盘时会发生什么?或者如果您想单独记录子系统。)

然而,依赖全局状态的最重要问题几乎总是在您尝试实现正确的 unit testing 时出现(您应该尝试这样做)。当您实际上无法访问的类的内容正在尝试进行无限制的磁盘写入和读取、连接到实时服务器并发送真实数据或从扬声器中发出声音时,处理您的应用程序变得非常困难任性。使用依赖注入要好得多,这样您就可以在测试计划的情况下模拟一个无所事事的类(并看到您需要在类构造函数中执行此操作)并将其指向,而无需猜测所有您的班级所依赖的全局状态。

相关链接:

Brittleness invoked by Global State and Singletons Dependency Injection to Avoid Singletons Factories and Singletons

模式使用与出现

模式作为想法和术语很有用,但不幸的是,当真正按照需要实现模式时,人们似乎觉得需要“使用”模式。通常,单例只是因为它是一种经常讨论的模式而被硬塞进去。设计你的系统时要注意模式,但不要仅仅因为它们存在就专门设计你的系统来适应它们。它们是有用的概念性工具,但正如您不会仅仅因为可以使用工具箱中的所有工具,您也不应该对模式做同样的事情。按需使用,不多也不少。

示例单实例服务定位器

#include <iostream>
#include <assert.h>

class Service 
public:
    static Service* Instance()
        return _instance;
    
    static Service* Connect()
        assert(_instance == nullptr);
        _instance = new Service();
    
    virtual ~Service()

    int GetData() const
        return i;
    
protected:
    Service()
    static Service* _instance;
    int i = 0;
;

class ServiceDerived : public Service 
public:
    static ServiceDerived* Instance()
        return dynamic_cast<ServiceDerived*>(_instance);
    
    static ServiceDerived* Connect()
        assert(_instance == nullptr);
        _instance = new ServiceDerived();
    
protected:
    ServiceDerived()i = 10;
;

Service* Service::_instance = nullptr;

int main() 
    //Swap which is Connected to test it out.
    Service::Connect();
    //ServiceDerived::Connect();
    std::cout << Service::Instance()->GetData() << "\n" << ((ServiceDerived::Instance())? ServiceDerived::Instance()->GetData() :-1);
    return 0;

【讨论】:

很好的答案。接受特殊细节。 +1 说得再好不过了,我从来没有写过一个我后来不后悔的单例,现在已经很多年没有写过。对于那些极其罕见的情况,你应该只允许一个类的单个实例,再次为多实例包装器+1。如果可以的话,我会投票 10 次。好伙伴。 "(...) 提供了一个封装接口,可以根据需要多次实例化,但只能访问全局状态。" -> 这基本上是一个“静态”工具箱。你也可以做一个普通的单身Toolbox。 我以前没有听说过这种模式,但您似乎看到了我的意图。我给了你一个赞成票。单例工具箱甚至service locator 需要警惕的一个问题是,您可以仍然在类的方法中隐藏它的使用,因此很难看出您的类依赖于来自接口的全局状态。我仍然建议通过类构造函数传入对工具箱或服务定位器的引用。 :) 我更新了服务定位器。在对单例进行了更多的个人体验之后,我发现了另一个避免使用它们的原因:单例无法实现继承,因为它在 go4 的设计模式中有所描述。【参考方案2】:

一个字:testing

可测试性的标志之一是类的松散耦合,允许您隔离单个类并对其进行完全测试。当一个类使用单例时(我说的是经典单例,它通过静态 getInstance() 方法强制执行它自己的奇点),单例用户和单例变得不可分割地耦合在一起。如果不测试单例,就无法再测试用户了。

单例测试是一场灾难。由于它们是静态的,因此您不能使用子类将它们存根。由于它们是全球性的,因此如果不重新编译或进行一些繁重的工作,您将无法轻松更改它们指向的引用。任何使用单例的东西都会神奇地获得对难以控制的东西的全局引用。这使得很难限制测试的范围。

【讨论】:

【参考方案3】:

我见过的 Singleton 最大的错误是你正在设计一个单用户系统(例如,一个桌面程序)并在很多事情上使用 Singleton(例如设置),然后你想成为多用户,例如网站或服务。

这类似于在多线程程序中使用具有内部静态缓冲区的 C 函数时发生的情况。

【讨论】:

【参考方案4】:

我会说不惜一切代价避免单身。它限制了应用程序的扩展。真正分析您处理的问题并考虑可扩展性,并根据您希望应用程序的可扩展性做出决策。

归根结底,如果设计不正确,单例会成为资源瓶颈。

有时您在没有完全了解这样做会对您的应用程序产生什么影响的情况下引入了这个瓶颈。

在处理尝试访问单例资源但陷入死锁的多线程应用程序时,我遇到了一些问题。这就是为什么我尽量避免使用单例。

如果您在设计中引入单例,请确保您了解运行时影响,绘制一些图表并找出可能导致问题的位置。

【讨论】:

【参考方案5】:

Singleton 通常被用作一个包罗万象的东西,人们不会费心用适当的访问器将它们正确封装在实际需要的地方。

最终结果是一个 tarball,它最终收集了整个系统中的所有 statics。这里有多少人从未在他们不得不使用的一些所谓的 OOD 代码中看到一个名为 Globals 的类?呃。

【讨论】:

【参考方案6】:

我不会在任何设计中完全避免它。但是,必须小心它的使用。它可以在许多情况下成为上帝的对象,从而破坏目的。

请记住,这种设计模式是解决一些问题但不是所有问题的解决方案。事实上,所有的设计模式都是一样的。

【讨论】:

【参考方案7】:

我不认为自己是一个经验丰富的程序员,但我目前的观点是你实际上不需要 Singleton ...是的,一开始似乎更容易使用(类似于全局变量),但随后出现了需要另一个实例的“哦,我的”时刻。

你总是可以传递或注入实例,我真的没有看到使用 Singleton 会更容易或有必要的情况

即使我们拒绝一切,代码的可测试性问题仍然存在

【讨论】:

【参考方案8】:

请参阅“What's Alternative to Singleton”讨论中的第一个答案。

【讨论】:

你的意思是this?您应该删除此答案并将此单行字添加为评论,或者实际评论您的链接以建立一个好的答案。 否决了@YevgeniyBrikman 的答案,因为 3 年后谁知道当时的“第一个答案”是什么?支持卡瓦斯。

以上是关于什么时候不应该使用单例模式? (除了显而易见的)的主要内容,如果未能解决你的问题,请参考以下文章

设计模式探秘之单例模式

关于单例模式中volatile的使用

请问java 单例类 与 静态类 有何不同?

单例设计模式

彻底理解单例模式

单例模式