什么时候不应该使用Singleton模式?(除了明显的)

Mik*_*ike 46 language-agnostic singleton design-patterns

我很清楚你想使用Singleton来提供对某些州或服务的全局访问点.在这个问题中不需要列举Singleton模式的好处.

我感兴趣的是Singleton一开始看起来不错的情况,但可能会回来咬你.一次又一次,我在书籍和海报上看到作者说,单身人士模式往往是一个非常糟糕的主意.

四人帮表示你想在以下情况下使用Singleton:

  • 必须只有一个类的实例,并且客户端必须可以从一个众所周知的访问点访问它.
  • 当唯一实例应该通过子类化可扩展时,客户端应该能够使用扩展实例而无需修改其代码.

这些要点虽然值得注意,但并不是我所寻求的实际问题.

有没有人有一套规则或警告你用来评估你是否真的,确定你想要使用Singleton?

M2t*_*2tM 68

摘要版本:

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

详细解答:

关于单身人士最重要的事情是它是全球状态.它是一种用于公开全局未缓存访问的单个实例的模式.这具有全局变量编程中的所有问题,但也采用了一些有趣的新实现细节,否则实际价值很小(或者实际上,单实例方面可能会产生不必要的额外成本).实现是不同的,当人们经常把它误认为面向对象的封装方法时,它实际上只是一个奇特的单实例全局.

您应该考虑单例的唯一情况是,具有多个已经全局数据的实例实际上是逻辑或硬件访问错误.即使如此,你应该通常不直接处理单,而是提供一个包装接口允许实例化多次,你需要它,但只访问全局状态.通过这种方式,您可以继续使用依赖注入,如果您可以从类的行为中取消全局状态,则不会对系统进行彻底更改.

然而,当看起来好像你不依赖于全局数据时,这有一些微妙的问题.所以(使用依赖注入包装单例的接口)只是一个建议,而不是一个规则.一般情况下它仍然更好,因为至少你可以看到该类依赖于单例,而只是在类成员函数的腹部内使用:: instance()函数隐藏了这种依赖.它还允许您提取依赖于全局状态的类并为它们进行更好的单元测试,并且您可以传入模拟无所事事的对象,如果您直接将单例依赖于单例,这将更加困难.

烘焙singleton :: instance调用时,它也会将自身实例化为类,这使得继承成为不可能.解决方法通常会破坏单例的"单个实例"部分.考虑一种情况,即您有多个项目依赖于NetworkManager类中的共享代码.即使你想这NetworkManager已是全球性的状态,单个实例,你应该了解使它成为一个单持怀疑态度.通过创建一个实例化自身的简单单例,您基本上无法从该类派生任何其他项目.

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

可接受的单例(read:servicelocator)的一个具体示例可能是包装单实例c样式接口,如SDL_mixer.单例通常天真实现的一个例子是它可能不应该在日志记录类中(当您想要登录到控制台和磁盘时会发生什么?或者如果您想单独记录子系统.)

然而,当您尝试实施适当的单元测试(并且您应该尝试这样做)时,依赖全局状态的最重要问题几乎总是会出现.如果你真正无法访问的课程大量尝试进行无限制的磁盘写入和读取,连接到实时服务器并发送真实数据,或者从扬声器中发出声音,那么处理应用程序变得非常困难威利聂.使用依赖注入是非常,更好,所以你可以在测试计划的情况下模拟一个do-nothing类(并且看看你需要在类构造函数中做到这一点)并指向它而不必将所有你的班级所依赖的全球国家.

相关链接:

模式使用与出现

模式作为想法和术语很有用,但不幸的是,当真正根据需要实现模式时,人们似乎觉得需要"使用"模式.单身人士通常只是因为它是一种常见的模式而被阉割.设计您的系统时要注意模式,但不要仅仅因为它们存在而专门设计您的系统.它们是有用的概念工具,但就像你不使用工具箱中的每个工具一样,你也不应该对模式做同样的事情.根据需要使用它们,不多也不少.

示例单实例服务定位器

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

  • +1不能说得更好,我从来没有写过一个我以后没有后悔的单身人士,现在已经多年没写过了.对于那些*非常罕见*的情况,多个实例包装器再次+1,你应该只允许一个类的单个实例.如果可以的话,我会投票10次.很好的一个伴侣. (3认同)

Pau*_*bel 12

一个字:测试

可测试性的一个标志是类的松散耦合,允许您隔离单个类并完全测试它.当一个类使用单例时(我正在谈论一个经典的单例,一个通过静态getInstance()方法强制它拥有奇点),单例用户和单例变得难以分开地耦合在一起.如果不测试单例,就不可能测试用户.

单身人士是一个需要考验的灾难.因为它们是静态的,所以不能用子类将它们存在.由于它们是全局的,因此无法轻松更改它们指向的引用,而无需重新编译或繁重.任何使用单身人士的东西都会神奇地获得对难以控制的东西的全局引用.这使得难以限制测试的范围.


Lou*_*nco 6

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

它与在多线程程序中使用的内部静态缓冲区的C函数发生的情况类似.


WeN*_*ers 5

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

在一天结束时,如果设计不正确,单身人员将成为资源瓶颈.

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

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

如果您在设计中引入单例,请确保您了解运行时的含义,做一些图并找出可能导致问题的位置.