Ank*_*kur 46 c++ singleton design-patterns
我最近几天一直在阅读关于Singleton模式的文章.一般认为,需要它的场景很少(如果不是很少见),可能是因为它有一些问题,例如
我开始了解这些问题背后的想法,但不完全确定这些问题.就像垃圾收集问题一样,在单例实现中使用静态(这是模式固有的),是关注点吗?因为这意味着静态实例将持续到应用程序.它是否会降低内存管理(它只是意味着分配给单例模式的内存不会被释放)?
当然,在多线程设置中,让所有线程都争用单例实例将是一个瓶颈.但是这种模式的使用如何导致同步问题(当然我们可以使用互斥或类似的东西来同步访问).
从(单元?)测试的角度来看,由于单身人士使用静态方法(很难被模拟或存根),他们可能会导致问题.对此不确定.有人可以详细说明这个测试问题吗?
谢谢.
car*_*arl 38
在典型的单例实现中,一旦创建了单例,就永远不会破坏它.当单身人士很小时,这种非破坏性质有时是可以接受的.但是,如果单例是巨大的,那么你不必要地使用比你想象的更多的内存.
在你有垃圾收集器(如Java,Python等)的语言中,这是一个更大的问题,因为垃圾收集器总是认为单例是必要的.在C++中,你可以通过delete指针作弊.然而,这会打开它自己的蠕虫,因为它应该是一个单例,但通过删除它,你可以创建第二个.
在大多数情况下,这种内存的过度使用不会降低内存性能,但可以认为它与内存泄漏相同.使用大型单件,您在用户的计算机或设备上浪费内存.(如果你分配一个巨大的单例,你可能会遇到内存碎片,但这通常是一个不关心的问题).
如果每个线程都在访问同一个对象并且您正在使用互斥锁,则每个线程必须等到另一个线程解锁单例.如果线程很大程度上依赖于单例,那么你会将性能降低到单线程环境,因为线程的大部分时间都在等待.
但是,如果您的应用程序域允许它,您可以为每个线程创建一个对象 - 这样线程就不会花时间等待而是完成工作.
值得注意的是,单例的构造函数只能测试一次.您必须创建一个全新的测试套件才能再次测试构造函数.如果您的构造函数不接受任何参数,这很好,但是一旦您接受了一个参数,您将无法再进行有效的单位测试.
此外,你不能有效地删除单例,并且你对模拟对象的使用变得难以使用(有很多方法,但它比它的价值更麻烦).继续阅读更多关于此...
单身人士也是设计不佳的标志.一些程序员希望使他们的数据库类成为单例."我们的应用程序永远不会使用两个数据库,"他们通常认为.但是,有一段时间可能有意义使用两个数据库,或者单元测试你会想要使用两个不同的SQLite数据库.如果您使用单例,则必须对应用程序进行一些重大更改.但是,如果您从一开始就使用常规对象,则可以利用OOP有效地按时完成任务.
大多数单例的情况都是程序员懒惰的结果.他们不希望将对象(例如,数据库对象)传递给一堆方法,因此它们创建一个单独的每个方法用作隐式参数的单例.但是,由于上述原因,这种方法很容易受到影响.
如果可以的话,尽量不要使用单身人士.虽然从一开始它们看起来似乎是一种很好的方法,但它通常总会导致设计不良并且难以维护代码.
Gre*_*ill 30
如果你还没有看到文章单身人士是病态的说谎者,你也应该阅读.它讨论了单例之间的互连如何从界面隐藏,因此构建软件所需的方式也隐藏在界面之外.
同一作者还有一些关于单身人士的其他文章的链接.
And*_*erd 22
在评估Singleton模式时,你必须问"有什么替代方案?如果我没有使用Singleton模式会发生同样的问题吗?"
大多数系统都需要Big Global Objects.这些是大而昂贵的项目(例如数据库连接管理器),或者保存普遍状态信息(例如,锁定信息).
Singleton的替代方法是在启动时创建此Big Global Object,并将其作为参数传递给需要访问此对象的所有类或方法.
非单身案件会发生同样的问题吗?让我们一个一个地检查它们:
内存管理:应用程序启动时将存在大全局对象,对象将一直存在直到关闭.由于只有一个对象,它将占用与单例情况完全相同的内存量.内存使用不是问题.(@MadKeithV:关机时的破坏顺序是另一个问题).
多线程和瓶颈:所有线程都需要访问同一个对象,无论它们是将此对象作为参数传递还是它们都调用了MyBigGlobalObject.GetInstance().所以Singleton与否,你仍然会遇到相同的同步问题(幸运的是有标准的解决方案).这也不是问题.
单元测试:如果您没有使用Singleton模式,那么您可以在每个测试开始时创建Big Global Object,并且垃圾收集器将在测试完成时将其取走.每个测试都将从一个新的,干净的环境开始,该环境不受之前测试的影响.或者,在Singleton案例中,一个对象通过所有测试,并且很容易变得"受污染".所以,是的,单例模式确实叮咬,当涉及到单元测试.
我的偏好:由于单独的单元测试问题,我倾向于避免使用Singleton模式.如果它是我没有单元测试的少数环境之一(例如,用户界面层),那么我可能会使用单身人士,否则我会避免使用它们.
我反对单身人士的主要论点基本上是他们结合了两个不好的属性.
你提到的事情可能是一个问题,当然,但他们并非必须如此.同步事物可以修复,如果许多线程经常访问单例,它只会成为瓶颈,依此类推.这些问题很烦人,但不是交易破坏者.
单身人士更根本的问题是,他们所做的事情从根本上说是糟糕的.
由GoF定义的单例有两个属性:
第一个应该很简单.总的来说,全球变量很糟糕.如果您不想要全局,那么您也不需要单例.
第二个问题不太明显,但从根本上说,它试图解决一个不存在的问题.
您最后一次意外实例化一个类是什么时候,您打算重用现有实例?
你最后一次不小心输入" std::ostream() << "hello world << std::endl" 是std::cout << "hello world << std::endl什么时候,当你的意思是 " "?
它不会发生.因此,我们首先不需要阻止这种情况.
但更重要的是,"只有一个实例必须存在"的直觉几乎总是错误的.我们通常的意思是"我目前只能看到一个实例的用途".
但"我只能看到一个实例的使用"与"如果有人敢于创建两个实例,应用程序将崩溃"不一样.
在后一种情况下,单身可能是合理的.但在前者中,它确实是一个不成熟的设计选择.
通常,我们最终需要多个实例.
您经常最终需要多个记录器.您可以在日志中编写干净,结构化的消息,供客户端监控,并且您可以将调试数据转储到自己使用的日志.
您也可以很容易地想到最终可能会使用多个数据库.
或程序设置.当然,一次只能激活一组设置.但是当它们处于活动状态时,用户可以进入"选项"对话框并配置第二组设置.他还没有应用它们,但是一旦他命中'ok',就必须换掉它们并替换当前活动的套装.这意味着,直到他命中'ok',实际上存在两组选项.
更一般地说,单元测试:
单元测试的基本规则之一是它们应该孤立运行.每个测试都应该从头开始设置环境,运行测试,并将所有内容拆除.这意味着每个测试都需要创建一个新的单例对象,对它运行测试并关闭它.
这显然是不可能的,因为单例创建一次,只创建一次.它无法删除.无法创建新实例.
所以最终,单身人士的问题并不是"很难让线程安全得到正确"这样的技术问题,而是更为根本的"他们实际上并没有对你的代码做出任何积极贡献.他们增加了两个特征,每个都是负面的,你的代码库.谁会想要那个呢?"