Singleton模式的问题

过去几天我一直在读Singleton模式。 一般认为,需要的场景很less(如果不是很less的话),可能是因为它有自己的问题,比如

  • 在垃圾收集环境中,对于内存pipe理来说可能是一个问题。
  • 在multithreading环境中,它可能会导致瓶颈并引入同步问题。
  • 来自testing的头痛。

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

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

从unit testing的angular度来看,由于单身人士使用静态方法(很难被嘲弄或扼杀),因此可能会导致问题。 不确定这一点。 有人可以详细说明这个testing问题吗?

谢谢。

在垃圾收集环境中,对于内存pipe理来说可能是一个问题

在典型的单例实现中,一旦你创build了单例,你永远不能销毁它。 单身小时,这种非破坏性有时是可以接受的。 但是,如果单身人士是巨大的,那么你不必要地使用更多的内存比你应该。

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

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

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

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

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

来自testing的头痛。

值得注意的是,一个单身的构造函数只能被testing一次。 您必须创build一个全新的testing套件才能再次testing构造函数。 如果你的构造函数没有使用任何参数,这很好,但是一旦你接受了一个参数,就不能再有效的单位testing了。

此外,你不能有效地将单例存根(stub)转换出来,并且你对模拟对象的使用变得很难使用(有办法可以解决这个问题,但是比它更值钱)。 继续阅读更多关于这个…

(这也导致了一个糟糕的devise!)

单身也是一个糟糕的devise的标志。 一些程序员想让他们的数据库类是一个单身人士。 他们通常认为,“我们的应用程序永远不会使用两个数据库”。 但是,有一段时间可能会使用两个数据库,或者unit testing您想要使用两个不同的SQLite数据库。 如果您使用单身人士,则必须对应用程序进行一些重大更改。 但是,如果您从一开始就使用常规对象,则可以利用面向对象的优势,高效,准时地完成任务。

单身人士的大多数情况是程序员懒惰的结果。 他们不希望将一个对象(例如数据库对象)传递给一堆方法,所以他们创build了一个单独的方法,每个方法都用作一个隐式参数。 但是,这种方法因为上述原因而叮当作响。

如果可以,尽量不要使用单身人士。 尽pipe从一开始看似乎是一个很好的方法,但通常总是会导致devise不佳,难以维护代码。

如果您还没有看到“ 单身人士是病态骗子 ”这篇文章,您也应该阅读。 它讨论了单元之间的互连如何从界面中隐藏起来,因此您需要构build软件的方式也从界面中隐藏起来。

有同一作者的单身的其他文章的链接。

在评估Singleton模式时,你必须问“有什么select?如果我不使用Singleton模式,会发生同样的问题吗?

大多数系统都需要大型全局对象 。 这些是大而昂贵的项目(例如数据库连接pipe理器),或者持有普遍状态信息(例如,locking信息)。

Singleton的替代方法是在启动时创build此Big Global对象,并将其作为parameter passing给需要访问此对象的所有类或方法。

同样的问题会发生在非单身的情况下吗? 我们一个接一个地检查一下:

  • 内存pipe理 :当应用程序启动时,大全局对象将存在,对象将一直存在,直到closures。 由于只有一个对象,它将占用与单例情况完全相同的内存量。 内存使用不是问题。 (@MadKeithV:关机时的销毁顺序是一个不同的问题)。

  • multithreading和瓶颈 :所有线程都需要访问同一个对象,无论它们是作为parameter passing了这个对象还是他们调用了MyBigGlobalObject.GetInstance() 。 所以Singleton与否,你仍然会有同样的问题,(幸运的是有标准的解决scheme)。 这也不是问题。

  • unit testing :如果你不使用Singleton模式,那么你可以在每个testing开始时创buildBig Global Object,当testing完成时垃圾收集器将会把它拿走。 每个testing都将从一个新的,干净的环境开始,这个环境不受前面testing的影响。 或者,在单身人士的情况下,一个客体将通过所有的testing,并且很容易被“污染”。 所以是的,在unit testing方面,Singleton模式真的会受到影响。

我的偏好:由于unit testing问题,我倾向于避免Singleton模式。 如果它是我没有unit testing(例如,用户界面层)的less数环境之一,那么我可能会使用单例,否则我会避免它们。

我对单身人士的主要观点基本上是把两个不好的性质结合在一起。

你提到的事情可能是一个问题,当然,但他们不一定是。 同步的东西可以是固定的,如果多个线程频繁访问单例,它只会成为一个瓶颈,等等。 这些问题令人讨厌,但不是交易破坏者。

单身人士更基本的问题是,他们试图做的是从根本上说是不好的。

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

  • 它是全球可访问的,而且
  • 它阻止了这个类不止一次被实例化。

第一个应该是简单的。 总的来说,全球化是不好的。 如果你不想要一个全局的,那么你也不想要一个单身。

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

你最后一次意外地实例化了一个类的时候,你想重用一个已经存在的实例吗?

当你最后一次意外地input“ std::ostream() << "hello world << std::endl “时,是什么时候意味着std::cout << "hello world << std::endl ”?

这只是不会发生。 所以我们不需要首先防止这一点。

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

但是“我只能看到一个实例的用法”和“如果有人敢创build两个实例,应用程序会崩溃”不一样。

在后一种情况下,单身人士可能是合理的。 但在前者,这是一个过早的deviseselect。

通常情况下,我们最终需要多个实例。

你通常最终需要多个logging器。 有你写的日志干净,结构化的消息,客户端监视,有一个你转储debugging数据为自己使用。

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

或程序设置。 当然,一次只能激活一组设置。 但是,当它们处于活动状态时,用户可以进入“选项”对话框并configuration第二组设置。 他还没有应用,但一旦他点击“OK”,他们必须交换,并更换当前活动的设置。 而这意味着,直到他被击中“好”,实际上存在两套select。

更一般地说,unit testing:

unit testing的基本规则之一是它们应该孤立运行。 每个testing都应该从头开始设置环境,运行testing,然后把所有东西都拆下来。 这意味着每个testing都希望创build一个新的单例对象,对它进行testing,并closures它。

这显然是不可能的,因为一个单身人士是创造一次,只有一次。 它不能被删除。 新的实例不能创build。

所以最终,单例问题不是像“难以确保线程安全正确”这样的技术性问题,而是一个更基本的“他们实际上并没有为你的代码贡献任何正面的东西,而是增加了两个负面的特质,到你的代码库,谁会想要这个?

关于这个unit testing的关注。 主要的问题似乎不是单独testing单例,而是testing使用它们的对象。

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

我同意以前的观点,经常使用这些观点,所以你不必在各处传递一个论点。 我这样做。 典型的例子是你的系统logging对象。 我通常会做一个单身人士,所以我不必把它通过整个系统。

调查 – 在日志logging对象的例子中,有多less人(显示手)会将额外的arg添加到任何可能需要logging日志的例程中 – 使用单例?

我不一定会把单身人士和全球人物等同起来。 任何事情都不应该阻止开发人员将单个对象的实例作为parameter passing给对象,而不是将它们召唤出来。 隐藏其全局可访问性的意图甚至可以通过隐藏其getInstance函数给几个select的朋友来完成。

至于unit testing的缺陷,单元意味着小,所以重新调用应用程序来testing单身人士不同的方式似乎是合理的,除非我错过了点的东西。