Problems with Singleton Pattern
最近几天我一直在读关于单体模式的文章。一般的看法是,需要它的场景很少(如果不是很少),可能是因为它有自己的一系列问题,例如
- 在垃圾收集环境中,这可能是与内存管理有关的问题。
- 在多线程环境中,它会导致瓶颈并导致同步问题。
- 预检引起的头痛。
我开始了解这些问题背后的想法,但不完全确定这些问题。就像在垃圾收集问题的情况下一样,在单例实现中使用静态(这是模式固有的),这是一个问题吗?因为这意味着静态实例将持续到应用程序。它是否会降低内存管理(这仅仅意味着分配给单例模式的内存不会被释放)?
当然,在多线程设置中,所有线程都在争用单线程实例将是一个瓶颈。但是,使用这种模式如何导致同步问题(当然,我们可以使用互斥体或类似的东西来同步访问)。
从(单位?)测试视角,因为单例使用静态方法(很难被模仿或存根化),它们会导致问题。对此不确定。有人能详细描述一下这个测试问题吗?
谢谢。
在垃圾收集环境中,它可能是与内存管理有关的问题。
在典型的单例实现中,一旦创建了单例,就永远不能破坏它。这种非破坏性的性质有时在单子很小的时候是可以接受的。然而,如果单例是大量的,那么您不必要地使用了比您应该使用的更多的内存。
在有垃圾收集器(如Java、Python等)的语言中,这是一个更大的问题,因为垃圾收集器总是相信单体是必需的。在C++中,可以通过EDCOX1对0指针进行欺骗。然而,这会打开它自己的蠕虫罐头,因为它应该是单体的,但是通过删除它,您可以创建第二个。
在大多数情况下,过度使用内存不会降低内存性能,但可以将其视为内存泄漏。对于一个大的单例,您将把内存浪费在用户的计算机或设备上。(如果您分配了一个巨大的单例,则可能会遇到内存碎片,但这通常是不关心的)。
在多线程环境中,它会导致瓶颈并导致同步问题。如果每个线程都在访问同一个对象,而您使用的是互斥体,则每个线程必须等待另一个线程解锁该单例。如果线程很大程度上依赖于单线程,那么您将降低单线程环境的性能,因为线程的大部分生命周期都在等待。
但是,如果您的应用程序域允许,您可以为每个线程创建一个对象——这样线程就不会花时间等待,而是工作。
预检引起的头痛。值得注意的是,单例的构造函数只能测试一次。为了再次测试构造函数,必须创建一个全新的测试套件。如果您的构造函数不接受任何参数,这是可以的,但是一旦您接受了参数参数,就不能再有效地进行单元测试。
此外,您不能有效地将单例排除在外,并且您对模拟对象的使用变得很难使用(有很多方法可以解决这个问题,但这比它的价值更麻烦)。继续阅读了解更多信息…
(这也导致了糟糕的设计!)单件也是设计糟糕的标志。一些程序员想让他们的数据库类成为单例。"我们的应用程序永远不会使用两个数据库,"他们通常认为。但是,有时使用两个数据库可能是有意义的,或者单元测试可能需要使用两个不同的sqlite数据库。如果使用单例,则必须对应用程序进行一些严重的更改。但是,如果您从一开始就使用常规对象,那么您可以利用OOP来高效、准时地完成任务。
单例的大多数情况都是程序员懒惰的结果。他们不希望将一个对象(如数据库对象)传递给一组方法,因此他们创建了一个单例,每个方法都将其用作隐式参数。但是,这种方法有上述原因。
如果可以的话,尽量不要用单件的。尽管从一开始它们看起来是一种好方法,但它通常会导致糟糕的设计和难以维护的代码。
如果你还没有看到《单身汉是病态骗子》这篇文章,你也应该读一下。它讨论了如何在接口中隐藏单例之间的互连,因此您需要构建软件的方式也隐藏在接口中。
有一些链接指向同一作者关于单件作品的其他几篇文章。
在评估单例模式时,您必须问"备选方案是什么?"如果我不使用单例模式,同样的问题会发生吗?"
大多数系统都需要大型全局对象。这些是大而昂贵的项目(例如数据库连接管理器),或者包含普遍的状态信息(例如,锁定信息)。
单件的替代方法是在启动时创建这个大型全局对象,并将其作为参数传递给需要访问该对象的所有类或方法。
同样的问题会在非单例情况下发生吗?让我们逐个检查它们:
内存管理:当应用程序启动时,将存在大型全局对象,并且该对象将一直存在,直到关闭。由于只有一个对象,它将占用与单例情况完全相同的内存量。内存使用不是问题。(@madkeithv:order of destruction at shutdown是另一个问题)。
多线程和瓶颈:所有的线程都需要访问同一个对象,不管它们是作为参数传递给这个对象还是称为myBigGlobalObject.getInstance()。所以不管是单件还是非单件,您仍然会遇到相同的同步问题(幸运的是,这些问题有标准的解决方案)。这也不是问题。
单元测试:如果您不使用单例模式,那么您可以在每个测试开始时创建大型全局对象,垃圾收集器将在测试完成时将其带走。每个测试将从一个新的、干净的环境开始,而这个环境不受上一个测试的影响。或者,在单例情况下,一个对象通过所有的测试,很容易被"污染"。所以是的,单例模式在单元测试中非常有用。
我的偏好:由于单是单元测试问题,我倾向于避免单例模式。如果这是我没有单元测试(例如,用户界面层)的少数环境之一,那么我可能会使用单例,否则我会避免使用单例。
我反对单子的主要论点是,它们结合了两个坏属性。
当然,你提到的事情可能是个问题,但它们不一定是。同步问题是可以解决的,只有当许多线程经常访问单例时,才会成为瓶颈,等等。这些问题很烦人,但不是交易破坏者。
单身汉更根本的问题是,他们试图做的事情从根本上来说是糟糕的。
根据GOF的定义,单例具有两个属性:
- 它是全球可访问的,并且
- 它防止类被多次实例化。
第一个应该很简单。一般来说,全球经济是不好的。如果你不想要一个全球性的,那么你也不想要一个单一的。
第二个问题不那么明显,但从根本上讲,它试图解决一个不存在的问题。
上一次意外地实例化类是什么时候,而您打算在那里重用现有实例?
你上一次不小心打"EDOCX1"〔0〕是什么时候,你的意思是"EDOCX1"〔1〕是什么时候?
这是不可能的。所以我们一开始不需要阻止。
但更重要的是,"只有一个实例"的直觉几乎总是错误的。我们通常的意思是"我目前只能看到一个实例的用途"。
但是"我只能看到一个实例的使用"与"如果有人敢创建两个实例,应用程序就会崩溃"不同。
在后一种情况下,单例可能是合理的。但在前一种情况下,这确实是一种过早的设计选择。
通常,我们最终需要不止一个实例。
你最终常常需要不止一个记录器。这里有一个日志,您可以将干净的、结构化的消息写入其中,供客户机监视,还有一个日志,您可以将调试数据转储到其中以供自己使用。
也很容易想象您最终可能会使用多个数据库。
或程序设置。当然,一次只能激活一组设置。但是,当它们处于活动状态时,用户可能会进入"选项"对话框并配置第二组设置。他还没有应用它们,但是一旦他点击"确定",它们就必须被交换并替换当前活动的集合。这意味着,在他点击"确定"之前,实际上存在两组选项。
更一般地说,单元测试:
单元测试的基本规则之一是它们应该独立运行。每个测试都应该从头开始设置环境,运行测试,然后销毁所有内容。这意味着每个测试都要创建一个新的singleton对象,对其运行测试,然后关闭它。
这显然是不可能的,因为一个单例创建一次,而且只创建一次。无法删除。无法创建新实例。
因此,归根结底,单例程序的问题并不是像"很难纠正线程安全性"这样的技术性问题,而是更基本的问题"它们实际上并没有为您的代码做出任何积极的贡献"。它们在代码库中添加了两个特性,每个都是负面的。谁会想要那个?"
关于这个单元测试问题。主要的问题似乎不在于测试单件子本身,而在于测试使用它们的对象。
由于这些对象依赖于隐藏和难以移除的单例,因此不能将它们隔离起来进行测试。如果singleton代表一个到外部系统(db连接、支付处理器、icbm发射单元)的接口,情况会更糟。测试这样一个物体可能会意外地写入数据库,发送一些钱谁知道在哪里,甚至发射一些洲际导弹。
我同意早先的观点,他们经常被使用,这样你就不必在各地进行争论。我做到了。典型的例子是系统日志记录对象。我通常会把它做成一个单件,这样我就不必把它传遍整个系统了。
调查——在日志对象的例子中,有多少人(举手示意)会在任何可能需要记录某个东西的例程中添加额外的参数——而不是使用单例?
我不一定把独生子和全球性的等同起来。没有什么可以阻止开发人员将对象的实例(singleton或其他)作为参数传递,而不是将其变为空的。隐藏其全局可访问性的目的甚至可以通过将其getInstance函数隐藏给几个选择的朋友来实现。
就单元测试缺陷而言,单元意味着很小,所以重新调用应用程序来测试单例的方法似乎是合理的,除非我遗漏了一些要点。