在一个项目中有很多单例类是好的编程实践吗?

4

我在一个项目中有几个类应该只创建一次。

最好的方法是什么?

  1. 它们可以被创建为静态对象。
  2. 可以被创建为单例模式。
  3. 可以被创建为全局对象。

最佳设计模式是什么?

我考虑将所有类都创建为单例模式,但这会创建大量单例。拥有大量单例是良好的编程实践吗?

使用单例模式的利弊是什么?


1
为什么它们只应该被创建一次? - Stefano Borini
2
请查看https://dev59.com/YXVC5IYBdhLWcg3w9GA9。 - PeterAllenWebb
3
然后,您应将它们作为指针传递给这些线程。如果它们不必是全局的,请不要将它们变成单例。 - GManNickG
1
@jalf,这个类怎么样?struct uhoh { uhoh(){static bool firstTime = true; if (!firstTime) make_computer_splode(); firstTime = false; } }; :) - GManNickG
查看 Programmers.SE上的同一问题其重复问题 获取更多意见。 - Franklin Yu
显示剩余6条评论
9个回答

18

好的,我刚刚浏览了这篇博客。我遇到了类似的问题,有像RegistryManager、FileManager、DataBaseManager等类,每个类只能被初始化一次,并且不同的线程可以设置这些类的状态,这些状态应该对所有线程都可见。 - anand
谢谢你提供的链接。这是一个很好的总结,也为我节省了不少时间。单例有时是不可避免的恶,但如果可能的话应该尽量避免使用,特别是当它们是可变的时候。 - Yann Ramin

7
如果它们只需要被创建一次,这并不意味着它们必须是单例。
- 如果X是一个单例,那么就暗示着只有一个实例。 - 如果X只有一个实例,这并不意味着它应该是单例。
仅仅因为你只需要一个实例,并不能成为使用单例的理由。全局变量是不好的,而单例只不过是全局变量的升级版。只有在你需要类的唯一实例并且它可以全局访问时才需要使用单例模式。大多数情况下,你并不需要使用单例模式。在糟糕的代码中,你会经常看到这种思想:“我只需要一个,那就意味着我应该把它做成单例!”(错误!)例如,我完成了迄今为止最强大的游戏引擎的技术设计。它有两个单例,用于内存和线程。这是一个非常庞大的项目,但我只有两个单例!
如果提供更多的上下文信息,我们将能够给您提供更好的建议。

2
虽然我不会称单例为“被吹捧的全局变量”,但我同意它们是带有额外、不必要且常常具有破坏性设计问题的全局变量。如果你需要让某个东西能够全局访问,就将它设为全局变量,而不是单例。 - jalf
然后你会得到一个更加复杂的实现来达到一个普通全局变量所提供的功能。仅仅因为可以解决单例模式带来的问题并不意味着它比一开始就避免这些问题更好。相较于全局变量,单例模式有什么优势呢? - jalf
1
单例模式相对于全局变量的问题在于只能创建一个实例。当你想要测试它时怎么办?祝你好运,在测试用例设置中实例化它。当你发现哎呀,你确实需要两个时怎么办?将“只能创建一个实例”这样的愚蠢和过早的限制放在你的代码上是不明智的。如果你想要懒加载的全局变量,那就实现它们,没有什么可以阻止你。但是不要费心去防止类的多个(非全局)实例化。 - jalf
我已经完成了迄今为止最强大的游戏引擎的技术设计。需要详细说明吗?或者有链接吗? - StackedCrooked
@GMan 我也有一个 singleton<T> 类。我猜你的和我的一样,在它的函数 instance() 中有一个静态 T。我发现在多线程应用程序中,如果没有互斥锁/锁/临界区/等等,这可能会导致它爆炸(实际上有时确实会)。为了避免 T 的双重初始化而不使用锁,你必须在 main 开始时或者你知道没有其他线程调用 instance() 时初始化你的单例。 - Gabriel
显示剩余8条评论

5
我建议您查看谷歌的Miško Hevery所做的一些视频和文章。首先是一个视频:“Clean Code Talks: Global State and Singletons”以及他的博客
普遍的共识是,在一些罕见的情况下,例如记录日志,单例模式是可以接受的,但在大多数其他情况下,您应该使用依赖注入。单例使得测试代码更加困难,并且它们隐藏了依赖关系,因此您的类不能轻松地独立实例化。

4

单例模式存在几个问题——难以测试、难以替换和难以扩展。通常有更好的方法。


3

我最喜欢的有关单例模式的文章之一是Miško Hevery的"Singletons are Pathological Liars"。基本上,它们鼓励“隐藏的”行为,这很难被学习和测试。


1

单例模式实际上是全局状态。如果你要创建很多单例,你就会创建很多全局状态,只是它看起来不像全局状态。

这使得构建单元测试、提供模拟类和重用代码变得困难,因为很容易将当前状态与函数耦合在一起。例如,当class X处于state Z时,函数foo才有效,否则它就无法工作。

正确地构建线程安全的单例也是有问题的。

单例可以用于协调对资源的访问,特别是那些没有太多状态且构造成本高昂的资源。

那么,为什么你认为你需要很多单例?如果你询问你的问题域和遇到的问题,你可能会得到更好的回答。


1

有些项目中,你无法避免使用全局变量。所有种类的服务定位器或依赖注入框架仍然依赖于对象的全局存储(不一定是静态变量,但始终是某种全局存储)。

但是,单例模式是问题的标志:

  • 首先,将单例作为规范模式与接口和抽象不兼容。虽然可以通过工厂访问来解决这个问题。
  • 更糟糕的是,单例模式是不灵活的 - 它没有任何识别对象类型之外的对象的手段。(在C++中,它们通过模板实现,但这是另一回事)。从这个意义上讲,它们实际上比静态变量还糟糕。从长远来看,最好使用一个框架,可以访问许多相同类型的实例。
  • 最重要的是,许多单例意味着许多对象之间的远程关系。这意味着您的系统可能比需要的复杂得多,并且将更加难以开发、测试和管理。简单地切换到定位器或DI在这里没有帮助,这是底层设计原则的问题。

DI垂直于这个问题。 DI确切地是通过工厂将依赖项注入对象的过程,但它并没有说明这些依赖项的本质。这里其他答案中的混乱可能来自于大多数DI框架都包括动态配置和服务定位器。这两者都可以不用DI使用,技术上DI也可以不使用它们。 - ima
请考虑使用DependencyManager.Register(Singleton::Instance())。 - ima

0
在编程中,并不存在万能的解决方案。将每个类都设为单例,不会让你的代码“更好”。单例是用来解决特定问题的工具,我正在学习更多关于单例的知识。

0

在项目中使用单例模式应该是一个经过深思熟虑和谨慎设计的决定,因为它是一条单向道,很少有回头的余地。我曾在一个多线程环境下的商业产品中实际使用过它,并面临了许多问题。但这并不意味着它是一个不可触及的模式。关键是任何可以通过单例实现的东西都可以在没有它的情况下实现,而且更少的麻烦和复杂性。如果想了解更多信息,请查看我几个月前提出的this question。它包含有关单例模式的有趣链接和见解。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接