Answers:
根据我的观察,对Singletons的两种主要批评分为两个阵营:
由于这两种结果,一种通用方法是使用创建一个广泛的容器对象来保存这些类的单个实例,并且只有容器对象才能修改这些类型的类,而许多其他类可以被授予访问权限以从容器对象。
我同意这是一种反模式。为什么?因为它允许您的代码依赖于其依赖关系,并且您不能相信其他程序员不会在以前的不可变单例中引入可变状态。
一个类可能有一个只接受字符串的构造函数,因此您认为它是单独实例化的,并且没有副作用。但是,无提示地,它正在与某种公共的,全局可用的单例对象进行通信,因此,每当实例化该类时,它都包含不同的数据。这是一个大问题,不仅对于您的API用户,而且对于代码的可测试性。为了正确地对代码进行单元测试,您需要进行微管理并了解单例中的全局状态,以获得一致的测试结果。
Singleton模式基本上只是一个延迟初始化的全局变量。全局变量通常被正确地认为是有害的,因为全局变量允许在程序看似无关的部分之间保持一定距离,以进行鬼动作。但是,恕我直言,将全局变量设置为程序初始化例程的一部分(例如,通过读取配置文件或命令行参数)并在其后将其视为常量,这些变量在一个位置一次设置就没有问题。全局变量的这种用法与在编译时声明命名常量的区别仅在于字母而不是实质。
同样,我对Singletons的看法是,当且仅当它们用于在程序的似乎无关的部分之间传递可变状态时,它们才是不好的。如果它们不包含可变状态,或者它们所包含的可变状态被完全封装,以使对象的用户即使在多线程环境中也不必了解它,那么它们就没有问题。
我已经在PHP世界中看到了很多单例。我不记得有任何用例可以证明这种模式是合理的。但是我想我知道人们为什么使用它的动机。
单实例。
“在整个应用程序中使用类C的单个实例。”
这是一个合理的要求,例如对于“默认数据库连接”。这并不意味着您将永远不会创建第二个数据库连接,而只是意味着您通常使用默认的数据库连接。
单实例化。
“不允许实例化C类多次(针对每个进程,针对每个请求等)。”
仅当实例化该类具有与其他实例冲突的副作用时,这才有意义。
通常,可以通过重新设计组件来避免这些冲突-例如,通过消除类构造函数的副作用。或者可以通过其他方式解决它们。但是可能仍然存在一些合法的用例。
您还应该考虑“仅一个”需求是否真的意味着“每个过程一个”。例如,对于资源并发,要求是“整个系统跨进程一个”,而不是“每个进程一个”。对于其他事情,它更确切地说是针对“应用程序上下文”,而您恰好每个进程只有一个应用程序上下文。
否则,无需执行此假设。强制执行此操作还意味着您无法为单元测试创建单独的实例。
全局访问。
仅当您没有适当的基础结构将对象传递到使用对象的地方时,这才是合法的。这可能意味着您的框架或环境很糟糕,但可能无法解决该问题。
价格紧密耦合,隐藏的依赖关系以及对全局状态不利的所有方面。但是您可能已经在遭受这些痛苦。
延迟实例化。
这不是单例的必要部分,但似乎是实现它们的最流行方法。但是,尽管拥有懒惰的实例化是一件好事,但您实际上并不需要单例实现。
典型的实现是具有私有构造函数,静态实例变量和带有延迟实例化的静态getInstance()方法的类。
除了上面提到的问题外,这还与单一职责原则紧密相关,因为除了该类已经具有的其他职责之外,该类还控制自己的实例化和生命周期。
在许多情况下,没有单例也没有全局状态,您可以获得相同的结果。相反,您应该使用依赖项注入,并且可能要考虑依赖项注入容器。
但是,在某些用例中,您还需要满足以下有效要求:
因此,在这种情况下,您可以执行以下操作:
使用公共构造函数创建要实例化的类C。
使用静态实例变量和带有延迟实例化的静态S :: getInstance()方法创建一个单独的类S,该实例将使用类C。
从C的构造函数中消除所有副作用。相反,请将这些副作用放入S :: getInstance()方法。
如果您具有多个满足上述要求的类,则可以考虑使用一个小的本地服务容器来管理类实例,并且仅将静态实例用于该容器。因此,S :: getContainer()将为您提供一个惰性实例化的服务容器,并且您可以从该容器中获取其他对象。
避免在可能的地方调用静态getInstance()。尽可能使用依赖项注入。特别是,如果将容器方法与多个相互依赖的对象一起使用,则这些对象都不必调用S :: getContainer()。
(可选)创建一个类C实现的接口,并使用该接口记录S :: getInstance()的返回值。
(我们是否仍将其称为单例?我将其留在评论部分。)
优点:
您可以创建一个单独的C实例进行单元测试,而无需触及任何全局状态。
实例管理与类本身分离->关注点分离,单一责任原则。
让S :: getInstance()为实例使用不同的类,甚至动态地确定要使用哪个类,都是很容易的。
就我个人而言,当我需要1、2或3或某些特定类的有限数量的对象时,我将使用单例。或者,我想传达给班级的用户,我不想让班级的多个实例正常运行。
另外,我只会在需要在代码中几乎所有地方使用它并且不想将对象作为参数传递给需要它的每个类或函数时使用它。
另外,如果不破坏其他函数的参照透明性,我将仅使用单例。意味着给定一些输入,它将始终产生相同的输出。即我不将其用于全局状态。除非可能全局状态被初始化一次且永不更改。
至于什么时候不使用它,请参阅上面的3并将其改为相反的方向。