it-swarm.cn

Singleton什么时候合适?

有人认为 Singleton Pattern 始终是反模式。你怎么看?

67
Fishtoaster

根据我的观察,对Singletons的两种主要批评分为两个阵营:

  1. 单例被能力较弱的程序员滥用和滥用,因此一切都变成了单例,您会看到代码中散布着Class :: get_instance()引用。一般来说,只有一个或两个资源(例如,数据库连接)符合使用Singleton模式的条件。
  2. 单例本质上是静态类,依赖于一个或多个静态方法和属性。当您尝试进行单元测试时,所有静态的东西都会带来实际的,切实的问题,因为它们代表了无法模拟或存根的代码中的死胡同。结果,当您测试依赖于Singleton的类(或任何其他静态方法或类)时,您不仅要测试该类,还要测试静态方法或类。

由于这两种结果,一种通用的方法是使用创建一个广泛的容器对象来保存这些类的单个实例,只有容器对象才能修改这些类型的类,而许多其他类可以被授予访问权限以从容器对象。

32
Noah Goodrich

我同意这是一种反模式。为什么?因为它允许您的代码依赖于其依赖关系,并且您不能相信其他程序员不会在以前的不可变单例中引入可变状态。

一个类可能有一个只接受字符串的构造函数,因此您认为它是单独实例化的,并且没有副作用。但是,无提示地,它正在与某种公共的,全局可用的单例对象通信,因此,每当实例化该类时,它都包含不同的数据。这是一个很大的问题,不仅对于您的API用户,而且对于代码的可测试性。为了正确地对代码进行单元测试,您需要进行微管理并了解单例中的全局状态,以获得一致的测试结果。

42
Magnus Wolffelt

Singleton模式基本上只是一个延迟初始化的全局变量。全局变量通常被正确地认为是有害的,因为它们允许在程序看似无关的部分之间保持一定距离,以进行鬼动作。但是,恕我直言,将全局变量设置为程序初始化例程的一部分(例如,通过读取配置文件或命令行参数)并在其后将其视为常量,这些变量在一个位置一次设置就没有问题。全局变量的这种用法与在编译时声明命名常量的区别仅在于字母而不是实质。

同样,我对Singletons的看法是,当且仅当它们用于在程序的看起来无关的部分之间传递可变状态时,它们才是不好的。如果它们不包含可变状态,或者它们所包含的可变状态被完全封装,以使对象的用户即使在多线程环境中也不必了解它,那么它们就没有问题。

34
dsimcha

人们为什么使用它?

我在PHP)世界中看到了很多单例。我不记得有任何用例可以证明这种模式是合理的。但是我想我对动机有一个了解人们确实使用它。

  1. 单个实例

    “在整个应用程序中使用类C的单个实例。”

    这是一个合理的要求,例如用于“默认数据库连接”。这并不意味着您将永远不会创建第二个数据库连接,而只是意味着您通常使用默认的数据库连接。

  2. 单个实例

    “不允许实例化C类多次(针对每个进程,针对每个请求等)。”

    仅当实例化该类具有与其他实例冲突的副作用时,这才有意义。

    通常,可以通过重新设计组件来避免这些冲突-例如通过消除类构造函数的副作用。或者可以通过其他方式解决它们。但是可能仍然存在一些合法的用例。

    您还应该考虑“仅一个”需求是否真的意味着“每个过程一个”。例如。对于资源并发性,要求是“每个系统跨进程一个”,而不是“每个进程一个”。对于其他事情,它更确切地说是针对“应用程序上下文”,而您恰好每个进程只有一个应用程序上下文。

    否则,无需执行此假设。强制执行此操作还意味着您无法为单元测试创​​建单独的实例。

  3. 全局访问。

    仅当您没有适当的基础结构将对象传递到使用对象的地方时,这才是合法的。这可能意味着您的框架或环境很糟糕,但可能无法解决该问题。

    价格紧密耦合,隐藏的依赖关系以及对全局状态不利的所有方面。但是您可能已经在遭受这些痛苦。

  4. 延迟实例化。

    这不是单例的必要部分,但似乎是实现它们的最流行方法。但是,尽管懒惰实例化是一件不错的事情,但您实际上并不需要单例来实现。

典型实施

典型的实现是具有私有构造函数,静态实例变量和带有延迟实例化的静态getInstance()方法的类。

除了上面提到的问题外,这还与 单一责任原则 紧密相关,因为该类确实 控制着自己的实例化和生命周期 。承担班级已经承担的其他责任。

结论

在许多情况下,没有单例也没有全局状态,您可以获得相同的结果。相反,您应该使用依赖项注入,并且可能要考虑一个 依赖项注入容器

但是,在某些用例中,您还需要满足以下有效要求:

  • 单一实例(但非单一实例)
  • 全球访问(因为您使用的是青铜时代框架)
  • 懒惰的实例化(因为这很高兴)

因此,在这种情况下,您可以执行以下操作:

  • 使用公共构造函数创建要实例化的类C。

  • 使用静态实例变量和带有延迟实例化的静态S :: getInstance()方法创建一个单独的类S,该实例将使用类C。

  • 从C的构造函数中消除所有副作用。相反,请将这些副作用放入S :: getInstance()方法。

  • 如果满足上述要求的类不止一个,则可以考虑使用一个 小型本地服务容器 管理这些类实例,并且仅将静态实例用于该容器。因此,S :: getContainer()将为您提供一个惰性实例化的服务容器,并且您可以从该容器中获取其他对象。

  • 避免在可能的地方调用静态getInstance()。尽可能使用依赖项注入。特别是,如果您将容器方法与多个相互依赖的对象一起使用,则这些对象都不必调用S :: getContainer()。

  • (可选)创建一个类C实现的接口,并使用该接口记录S :: getInstance()的返回值。

(我们是否仍将其称为单例?我将其留在评论部分。)

优点:

  • 您可以创建一个单独的C实例进行单元测试,而无需触及任何全局状态。

  • 实例管理与类本身分离->关注点分离,单一责任原则。

  • 让S :: getInstance()为实例使用不同的类,甚至动态地确定要使用哪个类,都是很容易的。

7
donquixote

就个人而言,当我需要1、2或3或数量有限的特定类的对象时,我将使用单例。或者,我想传达给班级的用户,我不想让班级的多个实例正常运行。

同样,我只会在需要在代码中几乎所有地方使用它并且不想将对象作为参数传递给需要它的每个类或函数时才使用它。

另外,如果不破坏其他函数的参照透明性,我将仅使用单例。意味着给定一些输入,它将始终产生相同的输出。即我不将其用于全局状态。除非可能全局状态被初始化一次且永不更改。

至于什么时候不使用它,请参阅上面的3并将其改为相反。

1
Brian R. Bondy