题 单身人真的那么糟糕吗? [重复]


可能重复:
单身人士有什么不好的? 

可以理解的是,许多设计模式在某些情况下可能被滥用,就像妈妈总是说:“太多好事并不总是好的!

我注意到这些天,我经常使用Singletons,我担心自己可能会滥用设计模式,并且越来越深入地研究一种不良习惯的习惯。

我们正在开发一个Flex应用程序,当用户使用它时,该应用程序在内存中保留了相当大的分层数据结构。用户可以按需加载,保存,更改和刷新数据。

这些数据通过Singleton类集中,该类聚合了几个ArrayCollections,Arrays,value对象以及通过getter和setter公开的一些其他本机成员变量。

要从应用程序的任何位置获取对数据的引用,我们执行整个Model.getInstance()方法类型的事情,我确信每个人都熟悉它。这确保了我们始终掌握相同的数据副本,因为在我们设计时,我们说在应用程序生命周期中只允许存在一次实例。

从这个中央数据存储库中,我们可以轻松地调度属性更改事件,并且可以有多个引用中央数据的UI组件,更新其显示以反映已发生的数据更改。

到目前为止,这种方法已经有效并且证明对我们的环境非常实用。

然而,我发现,在创建新课程时,我有点过分了。问题应该是一个类是Singleton,还是应该以其他方式进行管理,例如可能使用工厂,往往有点困难,有点不确定。

我在哪里画单线?是否有一个很好的指导方针来决定何时使用单身人士以及何时远离他们。

此外,任何人都可以推荐一本关于设计模式的好书吗?


47
2018-06-19 22:26


起源


对此有许多类似的问题,但我不确定它们中的任何一个是否可以被认为是“完全重复”...... - Zifre
3个人在5分钟内推荐同一本书必须是一个记录。并提示:-) - Stu
在任何情况下,问题是过剩(使用) - BlackTigerX
我的0.02美元是在Flex中,大多数坚固的设计原则都会因为你被迫跳过的愚蠢的箍而走出窗外。我指向HierarchicalCollectionView接口,作为一个主要的例子...... - rmeador


答案:


要记住的关键是设计模式只是帮助您理解抽象概念的工具。一旦掌握了这些理解,将自己专门限制在书中的“食谱”是毫无意义的,并且会损害您编写最适合您目的的代码的能力。

也就是说,阅读像GoF这样的书会给你提供更多思考问题的方法,这样当你自己实现某些东西的时候,你将有更广泛的视角来解决问题。

在你的情况下,如果在每种情况下使用单身人士都有意义,那么就去吧。如果它“有点”适合你必须以一种笨重的方式实现它,那么你需要提出一个新的解决方案。强制不完美的图案有点像在圆孔中敲击方形钉。

鉴于你说“这种方法已经有效并且证明对我们的环境非常实用”,我认为你做得很好。

这里有一些好书:

四人帮  - 设计模式的经典书籍

首先设计模式  - 我听过几个人推荐的这个替代方案


35
2018-06-19 22:29



非常感谢你们!我肯定会得到所有这些书的副本,并通过他们的方式。我想我已经听说过某个地方的“四人帮”。 - josef.van.niekerk
GoF书籍的大部分内容都很好,但就单身人士而言,我认真地认为他们一定是在吸食非法的东西。要么是这样,要么是老程序程序员(他们真的想要他们的全局变量)的让步,为了赢得他们的OOP(“嘿看,你也可以在OOP中制作全局变量!我们只称他们为单身人士”) - jalf
是的,我会说jalf所说的,谨防使用单身作为全球变量的oop版本。 - Simon P Stevens
Singleton始终是不可重新输入代码的标志,在大多数情况下,它也是不可测试代码的标志。只有在维护方面不受关注时,才能通过更快的上市时间证明它们的存在。甚至logger,在C ++中经典地被认为是有效的单例用例,当类从析构函数中记录并且单例记录器已经消失时,可能会导致应用程序崩溃。 - bobah


是的,单身人士很糟糕。它们很糟糕,因为它们为你所做的就是结合了两个属性,每个属性在95%的时间都很糟糕。 (这意味着平均而言,单身人士在99.75%的时间里表现不佳;))

由GoF定义的单例是一种数据结构:

  1. 授予对象的全局访问权限
  2. 强制只能存在一个对象实例。

第一个通常被认为是一件坏事。我们不喜欢全局变量。 第二个是更微妙,但一般来说,几乎没有任何情况下这是一个合理的限制 执行

有时,只有一个对象实例才有意义。在这种情况下,您选择只创建一个。您不需要单例来强制执行它。

通常情况下,即使只有一个实例“有意义”,它毕竟没有意义。迟早,你需要不止一个记录器。或者多个数据库。或者您将不得不为每个单元测试重新创建资源,这意味着我们必须能够随意创建它们。在我们理解后果之前,它过早地从我们的代码中消除了灵活性。

单例隐藏依赖关系并增加耦合(每个类都可能依赖于单例,这意味着除非我们还重用所有单例,否则该类不能在其他项目中重用),并且因为这些依赖关系不是立即可见的(作为函数/构造函数参数) ),我们没有注意到它们,通常在创建它们时不会考虑它们。只需插入一个单例就可以了,它几乎可以作为一个局部变量,所以我们倾向于在它们存在时使用它们很多。这使他们几乎不可能再次删除。你最终,也许不是意大利面条代码,而是意大利面依赖图。迟早,您的失控依赖关系将意味着单身人士开始依赖彼此,然后在尝试初始化时获得循环依赖关系。

它们使单元测试非常困难。 (如何测试在单个对象上调用函数的函数?我们不希望实际执行单例代码,但我们如何防止这种情况?

是的,单身人士很糟糕。

有时,你真的想要全球化。然后使用全局而不是单身。

有时,非常非常非常罕见,您可能会遇到创建类的多个实例是错误的情况 能够 没有造成错误就不能完成。 (关于我能想到的唯一一个案例,即使是设计的,如果你代表一些硬件设备。你只有一个GPU,所以如果你要将它映射到代码中的一个对象,它会理解只有一个实例可以存在)。但是,如果您发现自己处于这种情况(并且再次强调,多个实例导致严重错误的情况,而不仅仅是“我不能想到多个实例的任何用例”),那么执行这个约束,但是在没有使对象全局可见的情况下做到这一点。

这两个属性中的每一个 能够 在极少数情况下很有用。但我想不出一个案例在哪里 组合 他们将是一件好事。

不幸的是,很多人都认为“Singletons是符合OOP标准的全局变种”。不,他们不是。他们仍然遇到与全局, 此外 引入其他一些完全不相关的。绝对没有理由比普通的全球更喜欢单身人士。


107
2018-06-19 22:55



执行单身人士有很多次是一个关键的想法。想想数据库连接池。您可能不需要两个单独的池,因为当可能存在某些池时,您可能会打开连接。 - Kekoa
为什么我不想在单元测试中使用两个独立的池?为什么我不想要两个单独的池用于单独的数据库?如果我有两个独立的任务不应该让对方挨饿,为什么我不想要两个独立的池?一个任务可能会占用自己池中的所有连接,所以我想为另一个保留一些连接。但即使你 做 绝对肯定只想要一个游泳池,为什么你需要一个单身人士才能执行它?你经常吗? 偶然 创建新的连接池?当然不是。如果你只需要一个,那就是 创建 一。 - jalf
+1不能说得更好。究竟我的感受和教育我的同事们。许多要点也适用于单稳态模式和(ab)全局静态方法的使用。 - gix
问题1是真正的杀手,因为一旦你把它全部洒在你的代码上就很难修复。 - starblue
@user另一方面,你的同事常识可能和你的一样好。您编写了旨在存在一个实例的对象。他创建了第二个实例。你是谁这么说的 他错了?他遇到了一种情况,即创建第二个实例是有道理的,你没有预料到。如果它是一个单身人士,那么你的同事将无法做他认为有意义的事情。我的论点是“应该存在第二个实例吗?”是一个只有在课堂上才能合理做出的决定 用过的,而不是最初的设计。 - jalf


软件开发人员似乎很平均地分成两个阵营,这取决于他们是赞成理想主义的编码风格还是实用的编码风格:

  • 理想主义: 决不 使用单例模式。
  • 务实: 避免 单身模式。

就个人而言,我赞成务实的做法。有时违反规则是有道理的,但前提是你真正了解自己在做什么,并愿意接受相关的风险。如果您对以下关于特定用例的问题回答“是”,则单例模式可以产生一些实际好处。

  • 单身是你的应用程序的外部吗?数据库,排队服务和ESB都是单例模式的完全有效的宏示例。
  • KISS:你的整个应用仅限于2-3个内部单身人士吗?
  • DRY:这些单身人士本身是否具有全球性,因此不得不对您应用中的几乎所有对象进行探测? (例如,记录器或组件介体)?
  • 您的单身人士是否仅依赖于彼此和/或操作环境?
  • 您是否确保了每个单例的正确启动和关闭顺序,包括内存管理注意事项?例如,“Grand Central”样式的线程池可能需要在main()中具有实例Run()和Shutdown()方法,以便保证任务仅在它们操作的对象处于有效状态时运行。

14
2018-03-24 20:53



让你的单身成瘾更容易被社会接受的好方法。 “我避开它们,我只在有意义的地方使用它们”。不,你没有。根据您自己的描述,您可以随时使用它们,只要替代方案只需要少量工作。区别不在于“理想主义”和“务实”之间,而在于那些人之间 其实 尽量避免单身人士(我没有 需要 例如,我的数据库连接的单例,以及那些 说 他们“避免”他们,当他们真正意味着“当我认为这是正确的事情时,我会使用它们” - jalf


单身人士不会杀死程序,程序员会杀死程序。

像任何编程结构一样,如果使用得当,你就不会在脚下射击。

推荐的书很好,但是当你选择使用Singleton时,它们并不总能提供足够的经验。

只有在你需要拥有多个实例的时候发现Singleton是一个糟糕的选择时才能获得这种体验,而且突然间,你在各地注入对象引用时遇到了很多麻烦。

有时最好继续使用对象引用,但是如果你不得不将它重构为不同的设计,那么你使用Singleton的事实确实有助于确定你将遇到的问题的范围。我认为这是一件非常好的事情:即只是一个班级(即使设计不合理)也能提供一些能够看到班级改变效果的能力。


9
2018-06-19 22:42





我们已经启动了一个项目,我们基本上面临同样的问题,即如何 访问模型,尤其是它的根元素。该项目不是Flex应用程序,而是一个游戏!网络应用程序,但这并不重要。

有一个 单个对象唯一 在系统很好,问题是 如何访问它。所以关于单身人士的辩论与...的概念有关 依赖倒置 (DI),以及如何获取对象。

DI的主要论点如下:

  • 可测试性和嘲弄
  • 将对象实例化与使用分离(可以导致生命周期管理)
  • 关注点分离

DI的可能方法是(见经典 文章 来自福勒):

  • 在方法参数中传递对象
  • 服务定位器
  • DI框架

在这种观点下,单例模式只是一种服务定位器,例如, Model.getInstance()

但是为了在面对未来变化时提供最大的灵活性,应该引用唯一对象 传了过来 尽可能地,并获得 Model.getInstance() 只在必要时。这也将产生更清晰的代码。


4
2017-10-14 17:42



难道你最终没有额外的管道来支持传递引用? - kgriffs
...特别是对于几乎每个组件/类都要使用的东西,例如日志记录? - kgriffs


在我看来,Singletons的使用直接表明了设计缺陷。原因很简单,它们允许人们绕过C ++中内置的普通对象创建和销毁机制。如果一个对象需要引用另一个对象,它应该在构造时传入对它的引用,或者在内部创建它的新实例。但是当你使用单例时,你明确地模糊了创建和拆除周期。一个相关的问题是控制单身人士的生命是极其困难的。结果,许多包括通用单例实现的包也包括笨重的对象生命周期管理器等。有时候我想知道这些不存在仅仅是为了管理单身人士。

基本上,如果你需要在很多地方使用一个对象,它应该在堆栈中的最高公共点显式创建,然后通过引用向下传递给使用它的每个人。有时候人们使用Singletons是因为他们在将多个args传递给新线程时遇到了问题,但是不要为此而明确定义你的线程args并以相同的方式将它们传递给新线程。您会发现您的程序流程更清晰,并且由于静态初始化依赖性或错误的拆卸而没有令人讨厌的意外。


3
2017-08-14 00:20





单身人士肯定不错。他们有自己的用途,其中一些非常好。单身人士确实倾向于被没有经验的开发人员过度使用,因为它往往是他们所了解的第一个设计模式,并且它相当简单,因此他们在不考虑其含义的情况下将它放在各处。

每次要使用单例时,请尝试考虑为什么要这样做,以及使用此模式的好处和不利之处。

单身人士确实有效地创建了一套全球可访问的“东西”(数据或方法),我想大多数人会同意使用太多的全局变量并不是一个好主意。类和面向对象的整个要点是将事物分组为离散区域,而不是将所有东西都放入一个巨大的全局空间中。

我发现我倾向于偏爱单身的“模式”之一是从顶部传递所需的对象。我在应用程序初始化阶段创建它们一次,并将它们传递给需要访问它们的所有对象。它模仿单身模式的“单一创造”部分,但没有“全局”部分。

单例的全部意义在于它只适用于只存在1的对象。您提到了一组数据控件。也许考虑到实际上,有些情况下应用程序可能想要创建2组数据控件类,因此在这方面强制执行单例也不太对。相反,如果您在app init上创建了这些数据类并将其传递下来,那么您将只创建1个设置,因为这是您当前的应用程序所需要的,但是您可以保留在某些时候,如果您需要第二个设置的可能性你可以轻松创建它们。此外,数据控制类应该可以从应用程序的任何位置访问全局。我认为不是,它们应该只能从较低级别的数据访问层访问。

有些人推荐了GOF书。我会说,是的,这是一本很棒的书,但首先尝试先找一本关于通用架构的书,先阅读2/3 / n层设计,封装,抽象和这些原则。这将为您提供更坚实的基础,以便了解GOF谈论的模式的适当用法。

[编辑:另一个单例变体可能有用的时候是你想要一个单一的访问点,但实现细节可能实际上不止一件事。调用者不需要知道,在封面下,他们对单例对象的请求实际上是针对几个可用对象解决的,并且返回了一个。我在想这里的线程池,在哪里使用,嘿,只是给我一个线程,我需要1,但我不关心哪一个]


2
2018-06-19 22:51



线程池是单身人员回火的一个很好的例子。 .NET线程池就是这样工作的,这很痛苦。这意味着我无法为我的应用程序中的任务创建线程池。我必须与.NET类库,我可能正在使用的任何第三方库共享它,而且上帝知道还有什么。我绝对没有办法告诉它有多大可能 有 一个免费的线程。一个理智的设计是创建一个线程池类 任何人 可以实例化,如果他们想要他们的 拥有 线程池,然后公开我们可以使用的单个全局线程池,当我们不关心有多少其他用户存在时。 - jalf
@jaif:我觉得你错过了他的线程点。它是你想要在主线程上完成的非常快速的简单脏任务,如果你关心获得一个免费线程,或者你想要更多控制,你可以使用Thread类或BackgroundWorker。如果您想要自己的个人用户管理线程池,您可以查看实现生产者 - 消费者模式(请参阅本页的下半部分: albahari.com/threading/part4.aspx)。线程池的重点在于它旨在平衡整个系统的异步工作,而不仅仅是你的应用程序。 - Simon P Stevens
为什么要 一世 当.NET已经尝试为我做这件事时,我必须自己实现这种模式吗?关键在于,如果他们没有将其硬编码为单例,那么它将完成整个系统的工作,而不是 此外 在我自己的应用程序中的较小范围内有用。这是单身人士毫无理由地消除灵活性的完美范例。我并没有错过线程池的重点,我说通过一个微小的修改,它会更普遍有用。 - jalf
如果你创建了多个线程池,他们将如何平衡自己。我认为你想要的是不同的东西。我看不出多个线程池如何工作。线程池 是 系统范围的线程池,用于平衡应用程序之间的工作 是 只是其中之一,所以单例是访问它的合适方式。也许,MS也应该实现生产者 - 消费者线程队列,但是它们不能提供所有东西。不管怎样,不要过多争论它,我我确信MS有他们这样做的理由,我们必须忍受好坏。 - Simon P Stevens