Singleton模式的问题

时间:2009-09-08 06:37:31

标签: c++ design-patterns singleton

过去几天我一直在阅读关于Singleton模式的文章。一般认为,需要的场景很少(如果不是罕见的话)可能是因为它有一系列问题,例如

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

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

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

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

感谢。

7 个答案:

答案 0 :(得分:38)

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

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

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

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

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

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

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

来自测试的头痛。

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

此外,你不能有效地删除单例并且你对模拟对象的使用变得难以使用(有很多方法可以解决这个问题,但它比它的价值更麻烦)。继续阅读更多内容......

(它也导致设计不好!)

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

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

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

答案 1 :(得分:30)

如果您还没有看过文章Singletons are Pathological Liars,那么您也应该阅读。它讨论了单例之间的互连如何从界面隐藏,因此构建软件所需的方式也隐藏在界面之外。

同一作者有关于单身人士的其他几篇文章的链接。

答案 2 :(得分:21)

在评估Singleton模式时,你必须问“替代方案是什么?如果我不使用Singleton模式会发生同样的问题吗?”

大多数系统都需要 Big Global Objects 。这些是大而昂贵的项目(例如数据库连接管理器),或者保存普遍状态信息(例如,锁定信息)。

Singleton的替代方法是在启动时创建此Big Global Object,并将其作为参数传递给需要访问此对象的所有类或方法。

非单身案件会发生同样的问题吗?让我们一个一个地检查它们:

  • 内存管理:应用程序启动时将存在大全局对象,对象将一直存在直到关闭。因为只有一个对象,它将占用与单例情况完全相同的内存量。内存使用不是问题。 (@ MadKeithV:关机时的破坏顺序是另一个问题。)

  • 多线程和瓶颈:所有线程都需要访问同一个对象,无论是将这个对象作为参数传递,还是调用 MyBigGlobalObject.GetInstance() 的。所以Singleton与否,你仍然会遇到相同的同步问题(幸运的是有标准的解决方案)。这也不是问题。

  • 单元测试:如果您没有使用Singleton模式,那么您可以在每次测试开始时创建Big Global Object,垃圾收集器会在测试完成。每个测试都将从一个新的,干净的环境开始,该环境不受之前测试的影响。或者,在Singleton案例中,一个对象通过所有测试,并且很容易变得“受污染”。所以,是的,在单元测试方面,Singleton模式确实咬了

我的偏好:由于单独的单元测试问题,我倾向于避免使用Singleton模式。如果它是我没有进行单元测试的少数环境之一(例如,用户界面层),那么我可能使用单身人士,否则我会避免使用它们。

答案 3 :(得分:8)

我反对单身人士的主要论点基本上是他们将两个不良财产结合起来。

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

单身人士更为根本的问题是他们所做的事情根本就是坏事。

由GoF定义的单身人士有两个属性:

  • 可全局访问,
  • 它可以防止永远的类被多次实例化。

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

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

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

您上次不小心输入“std::ostream() << "hello world << std::endl”的时间是什么时候意味着std::cout << "hello world << std::endl”?

它不会发生。因此,我们首先不需要 来防止这种情况发生。

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

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

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

通常,我们最终会想要多个实例。

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

很容易想象您最终可能会使用多个数据库。

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

更一般地说,单元测试:

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

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

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

答案 4 :(得分:3)

关于本单元的测试问题。主要问题似乎不是测试单身本身,而是测试使用它们的对象。

这些对象不能被隔离用于测试,因为它们依赖于单身,这些单体既隐藏又难以移除。如果单例表示外部系统(DB连接,支付处理器,ICBM发射单元)的接口,情况会更糟。测试这样的物体可能会意外地写入数据库,发送一些知道在哪里甚至发射一些洲际导弹的钱。

答案 5 :(得分:1)

我同意之前的观点,即经常使用它们,这样你就不必在整个地方传递论据。我做到了典型示例是您的系统日志记录对象。我通常会把它变成单身,所以我不必将它全部传递给整个系统。

调查 - 在日志记录对象的示例中,有多少人(举手)会向可能需要记录内容的任何例程添加额外的arg -vs-使用单例?

答案 6 :(得分:0)

我不一定将Singletons与Globals等同起来。没有什么能阻止开发人员将对象的实例,单例或其他方式作为参数传递,而不是将其从空中召唤出来。隐藏其全局可访问性的意图甚至可以通过将其getInstance函数隐藏给少数选择的朋友来完成。

就单元测试缺陷而言,Unit意味着小,所以重新调用应用程序以不同的方式测试单例似乎是合理的,除非我遗漏了一些重点。