单身人士:好的设计还是拐杖? [关闭]
Posted
技术标签:
【中文标题】单身人士:好的设计还是拐杖? [关闭]【英文标题】:Singletons: good design or a crutch? [closed] 【发布时间】:2010-09-05 22:12:26 【问题描述】:单例是一种备受争议的设计模式,所以我对 Stack Overflow 社区对它们的看法很感兴趣。
请提供您的观点的理由,而不仅仅是“单身人士适合懒惰的程序员!”
这是一篇关于这个问题的相当不错的文章,尽管它反对使用单例: scientificninja.com: performant-singletons.
有没有人有其他关于他们的好文章?也许是为了支持单例?
【问题讨论】:
【参考方案1】:为单身辩护:
它们没有全局变量那么糟糕,因为全局变量没有标准强制的初始化顺序,而且由于幼稚或意外的依赖顺序,您很容易看到不确定的错误。单例(假设它们是在堆上分配的)是在所有全局变量之后创建的,并且位于代码中一个非常可预测的位置。 它们对于资源惰性/缓存系统非常有用,例如连接到慢速 I/O 设备的接口。如果你智能地为一个慢速设备构建一个单例接口,并且没有人调用它,你就不会浪费任何时间。如果另一段代码从多个地方调用它,您的单例可以同时优化两者的缓存,并避免任何重复查找。您还可以轻松避免单例控制资源上的任何死锁情况。反对单例:
在 C++ 中,没有很好的方法可以在单例后自动清理。 有一些变通方法和稍微老套的方法来做到这一点,但没有简单、通用的方法来确保您的单例是总是调用析构函数。这在记忆方面并不是那么糟糕——为此目的,只需将其视为更多的全局变量。但是,如果您的单例分配了其他资源(例如锁定某些文件)并且不释放它们,则可能会很糟糕。我自己的看法:
我使用单例,但如果有合理的替代方案,请避免使用它们。到目前为止,这对我来说效果很好,而且我发现它们是可测试的,尽管需要测试更多的工作。
【讨论】:
第一点的假设是一个相当大的飞跃,通常堆分配不是如何创建单例的(至少在 C++ 中)。 我不确定,MadKeithV。有一个标准模式,其中成员函数,比如 MyClass* MyClass::get_instance() 检查静态成员指针,如果它为 NULL,则调用 new 来分配(在堆上)共享实例。单身人士在别处怎么生活? 关于 C++ 的缺点:看看 Meyers 单例。 @Tyler:我刚刚遇到下面的代码,它支持@MadKeithV 不使用堆作为单例的视图。class MySingleton public: static MySingleton &getInstance() static MySingleton instance; return instance; private: MySingleton(); ~MySingleton(); ;
。参考 - ***.com/questions/2593324/c-singleton-class
问题标签:“语言无关”。答案的第一点:“全局变量没有标准强制的初始化顺序,...单例(假设它们是在堆上分配的)是在所有全局变量之后创建的”。这些当然不是与语言无关的 cmets。【参考方案2】:
Google 有一个用于 Java 的 Singleton Detector,我相信它最初是作为一种工具,必须在 Google 生成的所有代码上运行。删除单例的简单原因:
因为他们可以进行测试 困难和隐藏你的问题 设计
有关更明确的解释,请参阅 Google 的“Why Singletons Are Controversial”。
【讨论】:
我怀疑说“必须在 Google 生成的所有代码上运行”有点牵强。 (一方面,他们可能使用其他语言。) 我相信 Java、Python 和 C++ 是谷歌认可的语言。 他们也可以使用javascript:steve-yegge.blogspot.com/2007/06/rhino-on-rails.html 谷歌也有会计部门;这是否意味着每个开发人员都应该得到一个?禁止某些构造的决定是 Google 的一项业务决策,它更多地反映了他们的内部环境。毫无疑问,一些手动调整的机器代码仍然在那里被玩弄,但出于商业原因,不仅仅是任何人都应该这样做。 去阅读一些谷歌代码,他们确实不遵守这条规则。【参考方案3】:单例只是一堆花哨的全局变量。
全局变量和单例一样有它们的用途,但是如果您认为使用单例而不是使用令人讨厌的全局变量(每个人都知道全局变量很糟糕),那么您很遗憾地被误导了。
【讨论】:
全局变量...和方法。一个全局对象甚至...... 单例不必是真正的全局对象。【参考方案4】:单例的目的是确保一个类只有一个实例,并提供一个全局访问点。大多数时候,重点是单个实例点。想象一下,如果它被称为 Globalton。这听起来不那么吸引人,因为这强调了(通常)全局变量的负面含义。
大多数反对单例的好论据都与它们在测试中存在的困难有关,因为为它们创建测试替身并不容易。
【讨论】:
那么为什么一个类要确保它只有一个实例呢?这几乎违背了面向对象编程的目的。 通常您只需要一个日志记录工具?或者您正在包装一些 voodoo dll,而您不想让 dll 加载两次? @Calyth 但 为什么 将其作为类的责任,当它显然是系统责任时。 (对象的任何一个实例都可以正常工作,但如果不满足一次唯一的活动约束,系统可能会失败) @RuneFS 因为我只需要在一个类中编写一次代码来强制执行单一性。如果系统强制执行它,我必须在系统中的任何地方编写代码来强制执行单一性。测试一个类的一种特定行为比测试 100 个不同的类要容易得多,以确保它们都反映一种行为。 @iheanyi 你没抓住重点。您已将责任置于“100 个类”中,这并不是一种使其成为系统责任的设计。我主张将责任放在负责实例化的系统的任何部分(IoC 或类似)【参考方案5】:在 Google 测试博客中,Miško Hevery 提供了三篇关于单身人士的不错的博客文章。
-
Singletons are Pathological Liars
Where Have All the Singletons Gone?
Root Cause of Singletons
【讨论】:
哇,真的很有趣,但我想要解决方案! :-) txn 我认为“解决方案”之一是依赖注入,它实际上为您处理实例注入,它使单例从反模式中解脱出来。 :) 看看 Guice! code.google.com/p/google-guice 1.第一篇文章有问题。您可以简单地将单例设置为构造函数参数并完全解决本文中的问题。 2. 单例不必是全局的。因此,将文章中的“共享对象”替换为 Singleton,一切仍然有效。 3. 只有当你读到第三篇文章时,他才对前两篇中声称的所有废话说:“哦,我指的坏事是对“单例”的访问是真正全局的设计模式。嗯呃。所以你写了 3 篇文章来简单地说,全局变量很糟糕,尤其是在以复杂方式使用时。【参考方案6】:单例不是可怕的模式,尽管它被误用很多。我认为这种误用是因为它是一种更简单的模式,而且大多数刚接触单例的人都被全局副作用所吸引。
Erich Gamma 曾说单例模式是他希望 GOF 书中没有包含的模式,这是一个糟糕的设计。我倾向于不同意。
如果使用该模式是为了在任何给定时间创建一个对象的单个实例,则该模式被正确使用。如果为了产生全局效果而使用单例,则说明使用不正确。
缺点:
在调用单例的整个代码中,您正在耦合到一个类 单元测试很麻烦,因为很难用模拟对象替换实例 如果由于需要多个实例而需要稍后重构代码,这比将单例类传递给使用它的对象(使用接口)更痛苦优点:
类的一个实例在任何给定时间点都表示。 按照设计,您强制执行此操作 在需要时创建实例 全局访问是一个副作用【讨论】:
“全局访问是一个副作用”我很高兴有人指出了这一点。全局方面可能是单例中被滥用最多的部分 我敢打赌,Eric Gamma 对单身人士的看法来自很多来之不易的经验。 这个答案应该在 IMO 上名列前茅。 @RobS 我不明白。如何在不访问全局实例的情况下使用 Singleton 并保持链接的优势?全局访问不是副作用。 @Jeffrey 这一点最好称为“全局参考”。原来的回答清楚地说明了这一点:“如果因为需要多个实例而需要稍后重构代码,那比将单例类传递给对象更痛苦”【参考方案7】:小鸡们喜欢我,因为我很少使用单例,而且当我这样做时,它通常是不寻常的。不,说真的,我喜欢单例模式。你知道为什么?因为:
-
我很懒。
不会出错。
当然,“专家”会大谈特谈“单元测试”和“依赖注入”,但这些都是野狗的肾脏负担。你说单例很难进行单元测试?没问题!只需将所有内容都公开,然后将您的班级变成一个有趣的全球善良之家。你还记得 1990 年代的汉兰达节目吗?单例有点像这样,因为: A. 它永远不会死; B. 只能有一个。所以不要再听那些 DI 小鬼了,放弃实现你的单例。这里有一些更充分的理由......
每个人都在这样做。 单例模式让你立于不败之地。 单格押韵带有“win”(或“fun”,具体取决于您的口音)。【讨论】:
【参考方案8】:我认为对于单例模式的使用存在很大的误解。这里的大多数 cmets 都将其称为访问全局数据的地方。我们需要在这里小心 - 单例模式不用于访问全局变量。
单例应该用于只有一个给定类的实例。 Pattern Repository 有很多关于 Singleton 的信息。
【讨论】:
【参考方案9】:与我共事过的一位同事思想非常单一。每当有类似经理或老板之类的东西时,他都会把它变成单身人士,因为他认为应该只有一个老板。每次系统接受一些新要求时,结果证明有完全正当的理由允许多个实例。
如果域模型规定(不是“建议”)有一个,我会说应该使用单例。所有其他情况只是一个类的偶然单个实例。
【讨论】:
【参考方案10】:我一直在想办法在这里拯救可怜的辛格尔顿,但我必须承认这很难。我很少看到它们的合法用途,并且在当前进行依赖注入和单元测试的驱动下,它们很难使用。它们无疑是使用设计模式进行编程的“货物崇拜”表现形式。我曾与许多从未破解过“GoF”书但他们知道“Singelton”的程序员一起工作,因此他们知道“模式”。
虽然我确实不同意 Orion,但大多数时候我看到单例过度使用它不是连衣裙中的全局变量,而更像是连衣裙中的全局服务(方法)。有趣的是,如果您尝试通过 CLR 接口在安全模式下使用 SQL Server 2005 中的 Singeltons,系统将标记代码。问题是您拥有超出可能运行的任何给定事务的持久数据,当然,如果您将实例变量设置为只读,您可以解决这个问题。
这个问题导致我一年进行了大量的返工。
【讨论】:
我一直不明白为什么人们如此“喜欢”这种模式。我想这是因为它“允许”您使用全局变量,从而省去了重新思考设计的麻烦。除了,当然,它没有。【参考方案11】:圣战!好的让我看看..上次我检查设计警察说..
单例很糟糕,因为它们阻碍了自动测试 - 无法为每个测试用例重新创建实例。 相反,逻辑应该在可以轻松实例化和测试的类 (A) 中。另一个类 (B) 应该负责约束创建。单一责任原则脱颖而出!应该是团队知识,您应该通过 B 访问 A - 一种团队约定。
我基本上同意..
【讨论】:
【参考方案12】:许多应用程序要求某个类只有一个实例,因此只有一个类实例的模式很有用。但是该模式的实现方式存在差异。
有 静态单例,其中类强制每个进程只能有一个类的实例(在 Java 中实际上每个 ClassLoader 一个)。另一种选择是只创建一个实例。
静态单例是邪恶的 - 一种全局变量。它们使测试变得更加困难,因为不可能完全隔离地执行测试。您需要复杂的设置和拆卸代码来在每次测试之间清理系统,并且很容易忘记正确清理某些全局状态,这反过来可能会导致测试中出现未指定的行为。
只创建一个实例很好。您只需在程序启动时创建一个实例,然后将指向该实例的指针传递给需要它的所有其他对象。依赖注入框架使这很容易——您只需配置对象的范围,DI 框架将负责创建实例并将其传递给所有需要它的人。例如,在 Guice 中,您将使用 @Singleton 注释该类,并且 DI 框架将只创建该类的一个实例(每个应用程序 - 您可以在同一个 JVM 中运行多个应用程序)。这使测试变得容易,因为您可以为每个测试创建一个新的类实例,并让垃圾收集器在不再使用时销毁该实例。没有全局状态会从一个测试泄漏到另一个测试。
更多信息: The Clean Code Talks - "Global State and Singletons"
【讨论】:
关于在测试之间对此类对象的清理,为什么需要复杂呢?为什么这个物体不能被摧毁,而一个新的物体在它的位置上旋转起来?对此没有便利是一个实现问题,而不是 Pattern 本身的问题。 问题是记住在测试后总是清理对象。此外,由于代码可能从任何地方访问静态单例,仅通过查看测试代码是不可能看到某个类是否使用静态单例的,因此很容易忘记一些依赖项。将此与依赖注入进行比较,其中所有依赖项都必须在构造函数中显式声明 - 如果忘记某些依赖项,代码甚至不会编译。【参考方案13】:单例作为实现细节很好。作为接口或访问机制的单例是一个巨大的 PITA。
不带参数返回对象实例的静态方法与仅使用全局变量略有不同。相反,如果一个对象引用了传入的单例对象,无论是通过构造函数还是其他方法,那么单例实际上是如何创建的都无关紧要,整个模式也无关紧要。
【讨论】:
【参考方案14】:这不仅仅是一堆花里胡哨的变量,因为它有几十个职责,比如与持久层通信以保存/检索有关公司的数据、处理员工和价格集合等。
我必须说,您并没有真正描述应该是单个对象的东西,而且除了数据序列化之外,它们中的任何一个都应该是一个单一的对象,这是值得商榷的。
我可以看到至少 3 组我通常会在其中设计的类,但我倾向于更小、更简单的对象,它们可以很好地完成一组狭窄的任务。我知道这不是大多数程序员的天性。 (是的,我每天都在做 5000 行类的怪物,我特别喜欢有人写的 1200 行方法。)
我认为重点在于,在大多数情况下,您不需要单刀,而通常您只是让您的生活变得更艰难。
【讨论】:
【参考方案15】:单例的最大问题是它们使单元测试困难,尤其是当您想要并行但独立地运行测试时。
第二个是人们通常认为带有双重检查锁定的延迟初始化是实现它们的好方法。
最后,除非您的单例是不可变的,否则当您尝试扩展应用程序以在多个处理器上的多个线程中运行时,它们很容易成为性能问题。在大多数环境中,竞争同步的成本很高。
【讨论】:
有人愿意分享拒绝投票的原因吗? 比尔,我不得不说我也想知道?我没有太多经验,但你关于并发问题的观点对我来说听起来合乎逻辑? 在惰性初始化中使用单例有什么问题,例如单例是一个助手?我不是说你错了,我只是在问。 问题在于使用双重检查锁定来初始化它们。请参阅en.wikipedia.org/wiki/Double-checked_locking - 例如,在 Java 中,简单的双重检查实现不是线程安全的,因此可能会导致难以诊断的故障。【参考方案16】:单例有其用途,但在使用和暴露时必须小心,因为它们是way too easy to abuse,很难真正进行单元测试,并且很容易基于两个相互访问的单例创建循环依赖。
然而,这很有帮助,因为当您想确保所有数据在多个实例之间同步时,例如,分布式应用程序的配置可能依赖单例来确保所有连接都使用相同的 up迄今为止的数据集。
【讨论】:
单身人士是混合责任。确保某物只有 X 个实例是系统的责任,无论对象的功能是什么,都是对象的责任。【参考方案17】:我发现你必须非常小心为什么你决定使用单例。正如其他人所提到的,这与使用全局变量本质上是相同的问题。您必须非常谨慎,并考虑使用一个可以做什么。
很少使用它们,通常有更好的方法来做事。我遇到过这样的情况,我用一个单例做了一些事情,然后在我发现它使事情变得更糟之后(或者在我想出了一个更好、更理智的解决方案之后,我不得不筛选我的代码以将其取出) )
【讨论】:
【参考方案18】:我已经多次将单例与 Spring 结合使用,并不认为它是拐杖或懒惰的。
这种模式允许我为一堆配置类型值创建一个类,然后在我的 Web 应用程序的多个用户之间共享该特定配置实例的单个(非可变)实例。
在我的例子中,单例包含客户端配置条件 - css 文件位置、数据库连接条件、功能集等 - 特定于该客户端。这些类通过 Spring 实例化和访问,并由具有相同配置的用户(即来自同一公司的 2 个用户)共享。 * **我知道这种类型的应用程序有一个名称,但它让我忽略了*
我觉得为应用程序的每个用户创建(然后进行垃圾收集)这些“常量”对象的新实例会很浪费。
【讨论】:
这是一个不错的方法。问题来自于单例的静态访问器。【参考方案19】:我正在阅读很多关于“Singleton”,它的问题,何时使用它等,这些是我到现在为止的结论:
Singleton 的经典实现与实际需求之间的混淆:只有一个类的实例!
实施起来通常很糟糕。如果您想要一个唯一的实例,请不要使用返回静态对象的静态 GetInstance() 方法的(反)模式。这使得一个类负责实例化自身的单个实例并执行逻辑。这打破了Single Responsibility Principle。相反,这应该由一个工厂类来实现,负责确保只存在一个实例。
在构造函数中使用它,因为它易于使用并且不能作为参数传递。这应该使用dependency injection 解决,这是实现良好且可测试的对象模型的绝佳模式。
不是TDD。如果您使用 TDD,则会从实现中提取依赖项,因为您希望测试易于编写。这使您的对象模型更好。如果您使用 TDD,则不会编写静态 GetInstance =)。顺便说一句,如果您考虑具有明确职责的对象而不是类,您将获得相同的效果 =)。
【讨论】:
投反对票的人能否说明投反对票的原因?感谢您的反馈!【参考方案20】:我真的不同意化装束中的一堆全局变量的想法。当用于解决正确的问题时,单例非常有用。让我给你一个真实的例子。
我曾经为我工作的地方开发了一个小软件,有些表格必须使用有关公司、员工、服务和价格的一些信息。在它的第一个版本中,每次打开表单时,系统都会不断地从数据库中加载数据。当然,我很快就意识到这种方法并不是最好的。
然后我创建了一个单例类,命名为company,它封装了这个地方的一切,在系统打开的时候已经完全填满了数据。
这不仅仅是一堆花里胡哨的变量,因为它有几十个职责,比如与持久层通信以保存/检索有关公司的数据、处理员工和价格集合等。
此外,它是一个固定的、系统范围的、易于访问的点来获取公司数据。
【讨论】:
我没有对此投反对票,但我想解释一下为什么有人可能投反对票。 Singlton 不是缓存数据的最佳/唯一方法。一个有几十个职责的类没有“正确”设计。 听起来你的单例应该完全是一个单独的程序。接口是它的服务层。【参考方案21】:单例非常有用,使用它们本身并不是一种反模式。然而,他们之所以声名狼藉,主要是因为他们强迫任何消费代码承认他们是单例的,以便与他们进行交互。这意味着,如果您需要“取消单一化”它们,对您的代码库的影响可能非常大。
相反,我建议将 Singleton 隐藏在工厂后面。这样,如果您将来需要更改服务的实例化行为,您可以只更改工厂而不是使用 Singleton 的所有类型。
更好的是,使用控制容器的反转!它们中的大多数允许您将实例化行为与类的实现分开。
【讨论】:
【参考方案22】:在 Java 中,单例的一个可怕的事情是,在某些情况下,您可以最终得到同一个单例的多个实例。 JVM 基于两个元素唯一标识:一个类的完全限定名,以及负责加载它的类加载器。
这意味着同一个类可以被两个类加载器在不知道彼此的情况下加载,并且应用程序的不同部分将具有与之交互的此单例的不同实例。
【讨论】:
如果同一个类被两个类加载器加载,它就不是同一个类。 如果类由类加载器 A 和 B 加载,但它们都实现了类加载器 C 加载的相同接口,那么您可以将类的实例从类加载器 A 和 B 传递给 C 的用户。所以该程序可以同时使用单例的两个实例。【参考方案23】:编写普通的、可测试的、可注入的对象,让 Guice/Spring/whatever 处理实例化。严重地。
这甚至适用于缓存或单例的任何自然用例。 没有必要重复编写代码来尝试强制执行一个实例的恐惧。让您的依赖注入框架来处理它。 (如果您还没有使用轻量级 DI 容器,我推荐 Guice)。
【讨论】:
以上是关于单身人士:好的设计还是拐杖? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章
与 Liferay 一起去还是不去?有啥好的、坏的和丑陋的? [关闭]