Singleton模式的问题

Ank*_*kur 46 c++ singleton design-patterns

我最近几天一直在阅读关于Singleton模式的文章.一般认为,需要它的场景很少(如果不是很少见),可能是因为它有一些问题,例如

  • 在垃圾收集环境中,它可能是内存管理方面的问题.
  • 在多线程环境中,它可能导致瓶颈并引入同步问题.
  • 来自测试的头痛.

我开始了解这些问题背后的想法,但不完全确定这些问题.就像垃圾收集问题一样,在单例实现中使用静态(这是模式固有的),是关注点吗?因为这意味着静态实例将持续到应用程序.它是否会降低内存管理(它只是意味着分配给单例模式的内存不会被释放)?

当然,在多线程设置中,让所有线程都争用单例实例将是一个瓶颈.但是这种模式的使用如何导致同步问题(当然我们可以使用互斥或​​类似的东西来同步访问).

从(单元?)测试的角度来看,由于单身人士使用静态方法(很难被模拟或存根),他们可能会导致问题.对此不确定.有人可以详细说明这个测试问题吗?

谢谢.

car*_*arl 38

在垃圾收集环境中,它可能是内存管理方面的问题

在典型的单例实现中,一旦创建了单例,就永远不会破坏它.当单身人士很小时,这种非破坏性质有时是可以接受的.但是,如果单例是巨大的,那么你不必要地使用比你想象的更多的内存.

在你有垃圾收集器(如Java,Python等)的语言中,这是一个更大的问题,因为垃圾收集器总是认为单例是必要的.在C++中,你可以通过delete指针作弊.然而,这会打开它自己的蠕虫,因为它应该是一个单例,但通过删除它,你可以创建第二个.

在大多数情况下,这种内存的过度使用不会降低内存性能,但可以认为它与内存泄漏相同.使用大型单件,您在用户的计算机或设备上浪费内存.(如果你分配一个巨大的单例,你可能会遇到内存碎片,但这通常是一个不关心的问题).

在多线程环境中,它可能导致瓶颈并引入同步问题.

如果每个线程都在访问同一个对象并且您正在使用互斥锁,则每个线程必须等到另一个线程解锁单例.如果线程很大程度上依赖于单例,那么你会将性能降低到单线程环境,因为线程的大部分时间都在等待.

但是,如果您的应用程序域允许它,您可以为每个线程创建一个对象 - 这样线程就不会花时间等待而是完成工作.

来自测试的头痛.

值得注意的是,单例的构造函数只能测试一次.您必须创建一个全新的测试套件才能再次测试构造函数.如果您的构造函数不接受任何参数,这很好,但是一旦您接受了一个参数,您将无法再进行有效的单位测试.

此外,你不能有效地删除单例,并且你对模拟对象的使用变得难以使用(有很多方法,但它比它的价值更麻烦).继续阅读更多关于此...

(它也会导致糟糕的设计!)

单身人士也是设计不佳的标志.一些程序员希望使他们的数据库类成为单例."我们的应用程序永远不会使用两个数据库,"他们通常认为.但是,有一段时间可能有意义使用两个数据库,或者单元测试你会想要使用两个不同的SQLite数据库.如果您使用单例,则必须对应用程序进行一些重大更改.但是,如果您从一开始就使用常规对象,则可以利用OOP有效地按时完成任务.

大多数单例的情况都是程序员懒惰的结果.他们不希望将对象(例如,数据库对象)传递给一堆方法,因此它们创建一个单独的每个方法用作隐式参数的单例.但是,由于上述原因,这种方法很容易受到影响.

如果可以的话,尽量不要使用单身人士.虽然从一开始它们看起来似乎是一种很好的方法,但它通常总会导致设计不良并且难以维护代码.

  • 有时'Singletons'就像保持配置一样.如果我想从文件中读取一些conf并将其放在一个全局对象中,我更喜欢Singletons. (4认同)
  • @ManikandarajS:......这正是我们曾经遇到的问题,当客户想要以编程方式"覆盖"配置参数时.当然,解决方案是用配置对象替换单例.记住:你可以拥有许多你可以拥有的东西,但你不能拥有许多你被迫拥有的东西. (2认同)

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模式.如果它是我没有单元测试的少数环境之一(例如,用户界面层),那么我可能会使用单身人士,否则我会避免使用它们.

  • 在我看来,关于内存管理的评论在C++中是错误的.例如,作为应用程序对象的成员对象(由框架定义)的"单例"具有*明确定义的*破坏时间.任何基于"静态"某地的单例实现都容易出现静态初始化/销毁命令惨败.我已经看到了一些非常难看的黑客,可以从其他单身人士的破坏者那里获得对单身人士的访问. (4认同)

jal*_*alf 8

我反对单身人士的主要论点基本上是他们结合了两个不好的属性.

你提到的事情可能是一个问题,当然,但他们并非必须如此.同步事物可以修复,如果许多线程经常访问单例,它只会成为瓶颈,依此类推.这些问题很烦人,但不是交易破坏者.

单身人士更根本的问题是,他们所做的事情从根本上说是糟糕的.

由GoF定义的单例有两个属性:

  • 它是全球可访问的,并且
  • 它可以防止类从曾经被实例化不止一次.

第一个应该很简单.总的来说,全球变量很糟糕.如果您不想要全局,那么您也不需要单例.

第二个问题不太明显,但从根本上说,它试图解决一个不存在的问题.

您最后一次意外实例化一个类是什么时候,您打算重用现有实例?

你最后一次不小心输入" std::ostream() << "hello world << std::endl" std::cout << "hello world << std::endl什么时候,当你的意思是 " "?

它不会发生.因此,我们首先不需要阻止这种情况.

但更重要的是,"只有一个实例必须存在"的直觉几乎总是错误的.我们通常的意思是"我目前只能看到一个实例的用途".

但"我只能看到一个实例的使用"与"如果有人敢于创建两个实例,应用程序将崩溃"不一样.

在后一种情况下,单身可能是合理的.但在前者中,它确实是一个不成熟的设计选择.

通常,我们最终需要多个实例.

您经常最终需要多个记录器.您可以在日志中编写干净,结构化的消息,供客户端监控,并且您可以将调试数据转储到自己使用的日志.

您也可以很容易地想到最终可能会使用多个数据库.

或程序设置.当然,一次只能激活一组设置.但是当它们处于活动状态时,用户可以进入"选项"对话框并配置第二组设置.他还没有应用它们,但是一旦他命中'ok',就必须换掉它们并替换当前活动的套装.这意味着,直到他命中'ok',实际上存在两组选项.

更一般地说,单元测试:

单元测试的基本规则之一是它们应该孤立运行.每个测试都应该从头开始设置环境,运行测试,并将所有内容拆除.这意味着每个测试都需要创建一个新的单例对象,对它运行测试并关闭它.

这显然是不可能的,因为单例创建一次,只创建一次.它无法删除.无法创建新实例.

所以最终,单身人士的问题并不是"很难让线程安全得到正确"这样的技术问题,而是更为根本的"他们实际上并没有对你的代码做出任何积极贡献.他们增加了两个特征,每个都是负面的,你的代码库.谁会想要那个呢?"

  • 从大局来看,这有什么不同?如果新的开发人员无法加入并信任他的同事对代码所说的话(例如,"只创建了一个实例",那么他将不得不手动双重检查其他所有内容.所以不,我认为你正在制造稻草人的论点.新人将不得不相信他的同事几乎所有其他事情,所以我真的不明白为什么他需要绝对的关于*这个*特殊问题的确认. (2认同)