编辑:
从另一个问题,我提供了一个答案,有很多关于单例的问题/答案的链接:
所以我读了单身人士的帖子:好的设计还是拐杖?
争论仍在激烈进行。
我认为单例是一种设计模式(有好有坏)。
单例的问题不在于模式,而在于用户(对不起大家)。每个人和他们的父亲都认为他们可以正确地实施一个(从我所做的许多采访来看,大多数人都不能)。另外,因为每个人都认为他们可以实现正确的单例,所以他们滥用模式并在不合适的情况下使用它(用单例替换全局变量!)
所以需要回答的主要问题是:
什么时候应该使用单例
如何正确地实现单例
我对本文的希望是,我们可以在一个地方(而不是谷歌和搜索多个站点)收集何时(以及如何)正确使用Singleton的权威来源。同样合适的是列出反用法和常见的坏实现,解释为什么它们不能工作,以及对于好的实现来说它们的缺点。
所以开始行动吧:
我会举起我的手,说这是我用的,但可能有问题。
我喜欢“Scott Myers”在他的书《Effective c++》中对这个主题的处理。
使用单例的好情况(不多):
日志框架
线程回收池
/*
* C++ Singleton
* Limitation: Single Threaded Design
* See: http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
* For problems associated with locking in multi threaded applications
*
* Limitation:
* If you use this Singleton (A) within a destructor of another Singleton (B)
* This Singleton (A) must be fully constructed before the constructor of (B)
* is called.
*/
class MySingleton
{
private:
// Private Constructor
MySingleton();
// Stop the compiler generating methods of copy the object
MySingleton(MySingleton const& copy); // Not Implemented
MySingleton& operator=(MySingleton const& copy); // Not Implemented
public:
static MySingleton& getInstance()
{
// The only instance
// Guaranteed to be lazy initialized
// Guaranteed that it will be destroyed correctly
static MySingleton instance;
return instance;
}
};
好的。让我们把一些批评和其他实现放在一起。
: -)
Meyers单例模式在大多数情况下工作得足够好,在某些情况下,寻找更好的模式并不一定值得。只要构造函数永远不会抛出,并且单例对象之间没有依赖关系。
单例对象是全局可访问对象(从现在开始是GAO)的实现,尽管并非所有的GAO都是单例对象。
日志记录器本身不应该是单例的,但是理想情况下,记录日志的方法应该是全局可访问的,以便将日志消息的生成位置与日志记录的位置或方式分离。
延迟加载/延迟计算是一个不同的概念,单例通常也实现了这一点。它自身也有很多问题,尤其是线程安全问题,如果出现异常而失败,那么在当时看来是个好主意,结果却并不是那么好。(有点像字符串中的COW实现)。
考虑到这一点,goa可以像这样初始化:
namespace {
T1 * pt1 = NULL;
T2 * pt2 = NULL;
T3 * pt3 = NULL;
T4 * pt4 = NULL;
}
int main( int argc, char* argv[])
{
T1 t1(args1);
T2 t2(args2);
T3 t3(args3);
T4 t4(args4);
pt1 = &t1;
pt2 = &t2;
pt3 = &t3;
pt4 = &t4;
dostuff();
}
T1& getT1()
{
return *pt1;
}
T2& getT2()
{
return *pt2;
}
T3& getT3()
{
return *pt3;
}
T4& getT4()
{
return *pt4;
}
它不需要那么粗糙地完成,显然,在包含对象的加载库中,您可能需要一些其他机制来管理它们的生命周期。(将它们放在加载库时获得的对象中)。
至于我什么时候使用单例对象?我用它们做了两件事
-一个单例表,指示哪些库已经被dlopen加载
-日志记录器可以订阅的消息处理程序,您可以向其发送消息。信号处理程序特别要求。
Meyers单例模式在大多数情况下工作得足够好,在某些情况下,寻找更好的模式并不一定值得。只要构造函数永远不会抛出,并且单例对象之间没有依赖关系。
单例对象是全局可访问对象(从现在开始是GAO)的实现,尽管并非所有的GAO都是单例对象。
日志记录器本身不应该是单例的,但是理想情况下,记录日志的方法应该是全局可访问的,以便将日志消息的生成位置与日志记录的位置或方式分离。
延迟加载/延迟计算是一个不同的概念,单例通常也实现了这一点。它自身也有很多问题,尤其是线程安全问题,如果出现异常而失败,那么在当时看来是个好主意,结果却并不是那么好。(有点像字符串中的COW实现)。
考虑到这一点,goa可以像这样初始化:
namespace {
T1 * pt1 = NULL;
T2 * pt2 = NULL;
T3 * pt3 = NULL;
T4 * pt4 = NULL;
}
int main( int argc, char* argv[])
{
T1 t1(args1);
T2 t2(args2);
T3 t3(args3);
T4 t4(args4);
pt1 = &t1;
pt2 = &t2;
pt3 = &t3;
pt4 = &t4;
dostuff();
}
T1& getT1()
{
return *pt1;
}
T2& getT2()
{
return *pt2;
}
T3& getT3()
{
return *pt3;
}
T4& getT4()
{
return *pt4;
}
它不需要那么粗糙地完成,显然,在包含对象的加载库中,您可能需要一些其他机制来管理它们的生命周期。(将它们放在加载库时获得的对象中)。
至于我什么时候使用单例对象?我用它们做了两件事
-一个单例表,指示哪些库已经被dlopen加载
-日志记录器可以订阅的消息处理程序,您可以向其发送消息。信号处理程序特别要求。
如果你是创建单例对象并使用它的人,不要将它设置为单例对象(这没有意义,因为你可以控制对象的奇点而不将其设置为单例对象),但如果你是一个库的开发人员,并且你只想向你的用户提供一个对象,这是有意义的(在这种情况下,你是创建单例对象的人,但你不是用户)。
单例对象所以使用它们作为对象,很多人访问单例直接通过调用方法返回它,但这是有害的因为你使你的代码知道对象是单身,我喜欢用单例对象,我通过构造函数传递它们,使用它们作为普通对象,通过这种方式,你的代码不知道是否这些对象是单例对象,这让依赖关系更加明确,它有助于重构……