关于设计模式:啥时候应该使用单例?

Posted

技术标签:

【中文标题】关于设计模式:啥时候应该使用单例?【英文标题】:On design patterns: When should I use the singleton?关于设计模式:什么时候应该使用单例? 【发布时间】:2010-09-18 16:40:44 【问题描述】:

荣耀的全局变量 - 成为荣耀的全局类。有人说打破了面向对象的设计。

给我一​​些场景,除了使用单例的好旧记录器。

【问题讨论】:

自从学习了erlang,我更喜欢那种方式,即不变性和消息传递。 这个问题有什么没有建设性的地方?我在下面看到建设性的回答。 依赖注入框架是一个非常复杂的单例,它给出对象...... 单例可以用作其他对象实例之间的管理器对象,因此应该只有一个单例实例,其他实例应该通过单例实例进行通信。 我有一个附带问题:任何 Singleton 实现也可以使用“静态”类(使用“工厂”/“init”方法)来实现 - 无需实际创建类的实例(您可以说静态类是一种单例实现,但是......) - 为什么要使用实际的单例(确保其单一的单个类实例)而不是静态类?我能想到的唯一原因可能是“语义”,但即使在这个意义上,Singleton 用例实际上并不需要定义上的“类->实例”关系......所以......为什么? 【参考方案1】:

在我探寻真相的过程中,我发现实际上很少有“可接受”的理由来使用 Singleton。

互联网上反复出现的一个原因是“日志记录”类(您提到过)。在这种情况下,可以使用 Singleton 来代替类的单个实例,因为日志记录类通常需要被项目中的每个类一遍又一遍地使用而令人作呕。如果每个类都使用这个日志类,依赖注入就变得很麻烦了。

日志记录是“可接受的”单例的一个具体示例,因为它不会影响代码的执行。禁用日志记录,代码执行保持不变。启用它,同样的。 Misko 在Root Cause of Singletons 中这样说,“这里的信息流向一种方式:从您的应用程序到记录器。即使记录器是全局状态,由于没有信息从记录器流入您的应用程序,记录器是可以接受的。”

我相信还有其他正当理由。 Alex Miller 在“Patterns I Hate”中谈到服务定位器和客户端 UI 也可能是“可接受的”选择。

Read more at Singleton I love you, but you're bringing me down.

【讨论】:

@ArneMertz 我猜this 就是那个。 为什么不能只使用全局对象?为什么它必须是单例? 我认为日志工具的静态方法? 当您需要管理资源时,最好使用单例。例如,Http 连接。您不想为单个客户端建立 100 万个 http 客户端,这是非常浪费和缓慢的。因此,具有连接池 http 客户端的单例将更快且资源友好。 我知道这是一个老问题,这个答案中的信息很棒。但是,当 OP 明确指定时,我无法理解为什么这是公认的答案:“给我一些场景,除了使用单例有意义的旧记录器。”【参考方案2】:

单身候选人必须满足三个要求:

控制对共享资源的并发访问。 系统的多个不同部分将请求对资源的访问。 只能有一个对象。

如果您提议的 Singleton 只有其中一个或两个要求,那么重新设计几乎总是正确的选择。

例如,打印机后台处理程序不太可能从多个位置(打印菜单)调用,因此您可以使用互斥锁来解决并发访问问题。

简单的记录器是可能有效的单例的最明显示例,但随着更复杂的记录方案,这可能会发生变化。

【讨论】:

我不同意第 2 点。第 3 点并不是一个真正的理由(仅仅因为你可以,并不意味着你应该),第 1 点是一个很好的观点,但我仍然认为它没有用。假设共享资源是磁盘驱动器或数据库缓存。您可以添加另一个驱动器或让数据库缓存专注于另一件事(例如一个线程专用表的缓存,另一个更通用)。 我想你错过了“候选人”这个词。单身候选人必须满足三个要求;仅仅因为某些东西符合要求,并不意味着它应该是单例。可能还有其他设计因素:) 后台打印程序不符合标准。您可能需要一个实际上不打印的测试打印后台处理程序来进行测试。 如果你有用不可变的树结构表示的世界数据,并且你想协调更改以管理并发性。这棵树会成为单例的候选者吗?【参考方案3】:

读取只应在启动时读取的配置文件,并将它们封装在 Singleton 中。

【讨论】:

类似于 .NET 中的 Properties.Settings.Default @Paul,“no-singleton 阵营”将声明配置对象应该简单地传递给需要它的函数,而不是使其全局可访问(也称为单例)。 不同意。如果将配置移到数据库中,一切都搞砸了。如果配置路径依赖于单例之外的任何东西,这些东西也需要是静态的。 @PaulCroarkin 你能详细解释一下这有什么好处吗? @rr- 如果配置移动到数据库,它仍然可以封装在配置对象中,该对象将传递给需要它的函数。 (P.S. 我不在“非单身”阵营)。【参考方案4】:

当您需要管理共享资源时,您可以使用单例。例如打印机后台处理程序。您的应用程序应该只有一个后台处理程序实例,以避免对同一资源的请求冲突。

或数据库连接或文件管理器等

【讨论】:

我听说过这个打印机后台处理程序示例,我认为它有点蹩脚。谁说我不能拥有多个后台处理程序?到底什么是打印机假脱机程序?如果我有不同种类的打印机不能冲突或使用不同的驱动程序怎么办? 这只是一个示例...对于任何人用作示例的任何情况,您都可以找到使示例无用的替代设计。让我们假设假脱机程序管理由多个组件共享的单个资源。它有效。 这是四人帮的经典例子。我认为用真实试用过的用例来回答会更有用。我的意思是你觉得单例是最好的解决方案。 我认为共享资源是一个过于宽泛的例子。当您无法注入“spooler”的故障实现时,您将如何测试使用 print spooler 的对象在面对故障假脱机程序时是否正常工作?在我的书中,虽然简短且没有信息,但接受的答案是一种更安全的方法 打印机假脱机程序到底是什么?【参考方案5】:

存储一些全局状态(用户语言、帮助文件路径、应用程序路径)的只读单例是合理的。小心使用单例来控制业务逻辑 - 单例几乎总是以多例结束

【讨论】:

用户语言只能是单例的,假设只有一个用户可以使用系统。 ……并且一位用户只会说一种语言。 @SamuelÅslund 如果这是一个公平的假设是桌面应用程序 @user253751 是的,直到它突然不复存在,将 Java 语言单例转换为可以支持国际化网站的东西需要大量工作。我发现使用单例作为参数通常是一个合理的折衷方案,通过在调用者中检索单例实例,使用它的函数可以单独测试并重用而没有太多麻烦,并且显然不需要传递全局设置在很长的调用堆栈中。许多语言支持可用于避免重复的默认参数。 @spectras 虽然我同意,但这实际上是一个常见的情况,例如即使用户说得更多,你最不想要的操作系统是屏幕上到处都是混合语言。【参考方案6】:

管理与数据库的连接(或连接池)。

我也会用它来检索和存储外部配置文件的信息。

【讨论】:

数据库连接生成器不就是工厂的例子吗? @Ken 在几乎所有情况下,您都希望该工厂成为单身人士。 @Federico,“no-singleton 阵营”将声明这些数据库连接应该简单地传递给需要它们的函数,而不是让它们全局可访问(又名单例)。跨度> 你真的不需要单例。可以注射。 @NestorLedon 这真的取决于你使用它的频率,它可以通过两种方式完成,但如果你在应用程序的 99% 中使用某些东西,依赖注入可能不是大大地。另一方面,如果你只是偶尔使用它,但它仍然应该是“相同的”“事物”,那么 dep.inj。可能是这样:)【参考方案7】:

在管理对整个应用程序共享的资源的访问时,应使用单例,并且可能具有同一类的多个实例将是破坏性的。确保线程安全地访问共享资源是这种模式至关重要的一个很好的例子。

使用单例时,您应该确保不会意外隐藏依赖项。理想情况下,单例(就像应用程序中的大多数静态变量一样)在执行应用程序的初始化代码期间设置(静态 void Main() 用于 C# 可执行文件,静态 void main() 用于 java 可执行文件),然后传递给所有其他需要它的实例化类。这有助于您保持可测试性。

【讨论】:

【参考方案8】:

使用单例的方法之一是覆盖一个实例,其中必须有一个“代理”控制对资源的访问。单例在记录器中很不错,因为它们代理访问,例如,一个文件,该文件只能以独占方式写入。对于诸如日志记录之类的东西,它们提供了一种将写入抽象到日志文件之类的东西的方法——您可以将缓存机制包装到您的单例等等...

还可以考虑这样一种情况,即您的应用程序具有许多窗口/线程/等,但需要单点通信。我曾经用一个来控制我希望我的应用程序启动的作业。单例负责序列化作业并将其状态显示给程序的任何其他感兴趣的部分。在这种情况下,您可以将单例视为在您的应用程序中运行的“服务器”类...... HTH

【讨论】:

记录器通常是单例,因此记录对象不必被传递。任何体面的日志流实现都将确保并发写入是不可能的,无论它是否是单例。【参考方案9】:

我认为单例使用可以被认为与数据库中的多对一关系相同。如果您的代码中有许多不同的部分需要处理一个对象的单个实例,那么使用单例就很有意义。

【讨论】:

【参考方案10】:

当您从数据库或文件加载配置属性对象时,将其作为单例会有所帮助;没有理由继续重新读取在服务器运行时不会更改的静态数据。

【讨论】:

为什么不只加载一次数据并根据需要传递配置对象? 路过是怎么回事???如果我必须传递我需要的每个对象,我将拥有带有 20 个参数的构造函数...... @Enerccio 如果您的对象依赖于 20 个不同的其他对象而没有封装,那么您已经遇到了重大的设计问题。 @spectras 我可以吗?如果我实现 gui 对话框,我将需要:存储库、本地化、会话数据、应用程序数据、小部件父级、客户端数据、权限管理器等等。当然,您可以汇总一些,但为什么呢?就我个人而言,我使用 spring 和 aspect 将所有这些依赖项自动装配到小部件类中,从而将所有内容解耦。 如果你有这么多状态,你可以考虑实现一个外观,为特定上下文提供相关方面的视图。为什么?因为它允许在没有单例或 29-arg 构造函数反模式的情况下进行干净的设计。实际上,您的 gui 对话框访问所有这些东西的事实本身就是“违反单一责任原则”。【参考方案11】:

可以在Test::Builder 中找到一个单例的实际示例,该类支持几乎所有现代 Perl 测试模块。 Test::Builder 单例存储和代理测试过程的状态和历史记录(历史测试结果,计算测试运行的数量)以及测试输出的去向。这些都是协调由不同作者编写的多个测试模块以在单个测试脚本中协同工作所必需的。

Test::Builder 的单例的历史具有教育意义。调用new() 总是给你相同的对象。首先,所有数据都存储为类变量,对象本身没有任何内容。这一直有效,直到我想自己测试 Test::Builder。然后我需要两个 Test::Builder 对象,一个设置为虚拟对象,用于捕获和测试其行为和输出,另一个设置为真正的测试对象。那时 Test::Builder 被重构为一个真实的对象。单例对象存储为类数据,new() 将始终返回它。添加了create() 以创建新对象并启用测试。

目前,用户希望在自己的模块中更改 Test::Builder 的某些行为,但不理会其他行为,而测试历史记录在所有测试模块中保持相同。现在发生的事情是整体的 Test::Builder 对象被分解成更小的部分(历史、输出、格式......),其中一个 Test::Builder 实例将它们收集在一起。现在 Test::Builder 不再必须是单例。它的组成部分,就像历史一样,可以。这将单例的不灵活必要性降低了一个级别。它为用户提供了更大的灵活性来混合搭配作品。较小的单例对象现在可以只存储数据,它们的包含对象决定如何使用它。它甚至允许非 Test::Builder 类通过使用 Test::Builder 历史记录和输出单例来发挥作用。

似乎在数据协调和行为灵活性之间存在推拉关系,这可以通过将单例放在共享数据周围以尽可能少的行为来缓解,以确保数据完整性。

【讨论】:

【参考方案12】:

共享资源。特别是在 php 中,一个数据库类、一个模板类和一个全局变量 depot 类。所有这些都必须由在整个代码中使用的所有模块/类共享。

这是一个真正的对象用法 -> 模板类包含正在构建的页面模板,它被添加到页面输出的模块塑造、添加、更改。它必须作为单个实例保存,这样才能发生这种情况,数据库也是如此。使用共享数据库单例,所有模块的类都可以访问查询并获取它们,而无需重新运行它们。

全局变量库单例为您提供了一个全局、可靠且易于使用的变量库。它可以很好地整理您的代码。想象一下,将所有配置值放在一个单例中的数组中,例如:

$gb->config['hostname']

或将所有语言值放在一个数组中,例如:

$gb->lang['ENTER_USER']

在运行页面代码的最后,你会得到一个现在成熟的:

$template

单例,一个$gb 单例,其中包含用于替换的 lang 数组,并且所有输出都已加载并准备就绪。您只需将它们替换为成熟模板对象的页面值中现在存在的键,然后将其提供给用户。

这样做的最大优势是您可以对任何东西进行任何您喜欢的后期处理。您可以将所有语言值通过管道传输到谷歌翻译或其他翻译服务并将它们取回,并将它们替换到它们的位置,例如翻译。或者,您可以根据需要替换页面结构或内容字符串。

【讨论】:

您可能希望将答案分成多个段落并屏蔽代码段以提高可读性。【参考方案13】:

您可以在实现状态模式时使用 Singleton(以 GoF 书中所示的方式)。这是因为具体的 State 类没有自己的状态,并且根据上下文类执行它们的操作。

您也可以将抽象工厂设为单例。

【讨论】:

这是我现在在一个项目中处理的情况。我使用状态模式从上下文的方法中删除重复的条件代码。状态没有自己的实例变量。但是,对于是否应该将它们设为单例,我持观望态度。每次状态切换时都会实例化一个新实例。这看起来确实很浪费,因为实例不可能与另一个实例有任何不同(因为没有实例变量)。我想弄清楚为什么我不应该使用它。 @kiwicomb123 尝试让您的setState() 负责决定状态创建策略。如果您的编程语言支持模板或泛型,它会有所帮助。除了 Singleton,您可以使用 Monostate 模式,其中实例化状态对象最终会重用相同的全局/静态状态对象。更改状态的语法可以保持不变,因为您的用户不需要知道实例化状态是单态。 好的,所以在我的状态下,我可以将所有方法设为静态,所以每当创建一个新实例时,它不会有相同的开销?我有点困惑,我需要阅读有关 Monostate 模式的信息。 @kiwicomb123 不,Monostate 并不是要让所有成员都保持静态。最好先阅读它,然后查看相关问题和答案。 我觉得这应该有更多的选票。抽象工厂很常见,因为工厂是无状态的,稳定的无状态,并且不能用未覆盖的静态方法(在 Java 中)实现,所以使用单例应该没问题。【参考方案14】:

首先,让我们区分Single ObjectSingleton。后者是前者众多可能的实现之一。而且 Single Object 的问题与 Singleton 的问题不同。单一对象本身并不是坏事,有时是做事的唯一方法。简而言之:

单个对象 - 我只需要程序中的一个对象实例 Singleton - 创建一个具有静态字段的类。添加返回此字段的静态方法。在第一次调用时懒惰地实例化一个字段。始终返回相同的对象。
public class Singleton 
    private static Singleton instance;

    private Singleton() 

    public static Singleton instance() 
        if (instance == null) 
            instance = new Singleton();
        
        return instance;
    

如您所见,规范形式的“单例”模式对测试不太友好。不过,这很容易解决:只需让 Singleton 实现一个接口。我们称它为“可测试的单例”:)

public class Singleton implements ISingleton 
    private static Singleton instance;

    private Singleton() 

    public static ISingleton instance() 
        if (instance == null) 
            instance = new Singleton();
        
        return instance;
    

现在我们可以模拟 Singleton,因为我们通过接口使用它。其中一项索赔消失了。让我们看看我们是否可以摆脱另一个声明 - 共享全局状态。

如果我们剥离单例模式,它的核心就是延迟初始化:

public static ISingleton instance() 
    if (instance == null) 
        instance = new Singleton();
    
    return instance;

这就是它存在的全部原因。 这就是单对象模式。我们把它拿走,放到工厂方法中,例如:

public class SingletonFactory 
    private static ISingleton instance;

    // Knock-knock. Single Object here
    public static ISingleton simpleSingleton() 
        if (instance == null) 
            instance = new Singleton();
        
        return instance;
    

我们的可测试单例有什么不同?没有none,因为这是单对象模式的精髓——无论您将其实现为单例、工厂方法还是服务定位器都没有关系。你仍然有一些共享的全局状态。如果从多个线程访问它,这可能会成为一个问题。您必须使simpleSingleton() 同步并处理所有多线程问题。

再一次:无论您选择哪种方法,您都必须支付单一对象的价格。使用依赖注入容器只是将复杂性转移到必须处理单个对象固有问题的框架上。

回顾:

    大多数提到 Singleton 的人都是指 Single Object 一种流行的实现方式是单例模式 它有可以缓解的缺陷 然而,Singleton 的大部分复杂性源于 Single Object 的复杂性 无论您如何实例化单个对象,它仍然存在,无论是服务定位器、工厂方法还是其他东西 您可以将复杂性转移到经过(希望)经过良好测试的 DI 容器 有时使用 DI 容器很麻烦 - 想象一下为每个类注入一个 LOGGER

【讨论】:

【参考方案15】:

正如大家所说,共享资源——特别是不能处理并发访问的东西。

我见过的一个具体例子是 Lucene Search Index Writer。

【讨论】:

【参考方案16】:

当您想确保一个类将有一个实例并且该实例将具有对其的全局访问点时,您可以使用单例设计模式。

假设您有一个应用程序需要数据库来处理 CRUD 操作。理想情况下,您应该使用与数据库相同的连接对象来访问数据库并执行 CRUD 操作。

因此,为了确保数据库类有一个对象,并且在整个应用程序中都使用同一个对象,我们实现了单例设计模式。

确保您的构造函数是私有的,并且您提供了一个静态方法来提供对单例类的单个对象的访问

【讨论】:

【参考方案17】:

在处理可插拔模块时,我将它用于封装命令行参数的对象。主程序不知道要加载的模块的命令行参数是什么(甚至不总是知道正在加载哪些模块)。例如,主加载 A,它本身不需要任何参数(所以为什么它应该采用额外的指针/引用/其他,我不确定 - 看起来像污染),然后加载模块 X、Y 和 Z。两个其中,比如 X 和 Z,需要(或接受)参数,因此它们回调命令行单例以告诉它要接受哪些参数,并在运行时回调以查明用户是否确实指定了任何参数其中。

在许多方面,如果您每个查询只使用一个进程,则用于处理 CGI 参数的单例将类似地工作(其他 mod_* 方法不这样做,所以在那里会很糟糕 - 因此参数说你不应该在 mod_cgi 世界中使用单例,以防你移植到 mod_perl 或其他世界)。

【讨论】:

【参考方案18】:

将特定的基础架构问题配置为单例或全局变量可能非常实用。我最喜欢的例子是依赖注入框架,它使用单例作为框架的连接点。

在这种情况下,您将依赖基础架构来简化库的使用并避免不必要的复杂性。

【讨论】:

【参考方案19】:

我认为如果您的应用程序具有多个层,例如表示层、域和模型。 Singleton 非常适合成为横切层的一部分。并为系统中的每一层提供服务。

本质上,Singleton 封装了一个服务,例如日志记录、分析,并将其提供给系统中的其他层。

是的,单身人士需要遵循单一责任原则。

【讨论】:

【参考方案20】:

所以我正在阅读学校的单例模式,教授们整理了一份关于该主题的当前观点和最佳实践的列表。如果您构建单例以便它不会向代码添加任何内容,则似乎可以使用单例。如果你做到了,可以打开和关闭单例使用,除了工作负载之外几乎没有副作用,那么使用这种设计模式是安全且可取的。

【讨论】:

【参考方案21】:

单例模式是 Spring 容器化方法中最普遍的模式。如果我们从架构原语的角度来看 - 它们形成了一个对象的黑板图,每个线程都可以对其进行读写。他们执行在多个线程之间同步的戏剧性行为。多个线程需要同步的真正原因是因为始终存在作为计算程序基础的资源,在这些资源上可能会发生争用。考虑一下所谓的“最后一个座位问题”。正在预订航班,但有多种方法可以预订。为简单起见,假设有关航班占用的数据存储在平面文件而不是数据库中。现在,如果有两个线程,每个线程在功能上都不同(即由 webapp 中的不同端点表示),让这些线程 A 中的一个成为潜在乘客用来进行预订的线程,另一个 B 是航班经理用来关闭预订 - 几乎关闭登机门。然后,如果这些线程不使用单例,则飞行对象将从那里的真实资源中分离出来,我们说的不是实际的飞机,而是平面文件中的条目。 A线程会引用一个对象,而乘客还在纠结要不要飞,最后当他下定决心时,B线程已经关门了。但是 A 线程引用的对象仍然会显示多一个座位。现在,由于我们最初的假设,删除了 RDBMS,即使登机已关闭,系统也会为乘客写一张票并发给他。现在,在单例实现中,当读 B 访问系统时,通用对象 Flight 更新为关闭状态。因此,如果乘客最终下定决心并单击确认,他将立即出错。如果没有单例,这一切都是不可能的。因此,单例允许您靠近资源并避免线程争用。

【讨论】:

如果我们仔细观察,单例模式的使用降低了工厂模式的可能性。特别是在春季,不可能有任何值得一提的运行时多态性实现【参考方案22】:

也许是代码示例。

这里,ConcreteRegistry 是扑克游戏中的一个单例,它允许包树的所有行为访问游戏的少数核心接口(即模型、视图、控制器、环境等的外观) .):

http://www.edmundkirwan.com/servlet/fractal/cs1/frac-cs40.html

编。

【讨论】:

链接现在被破坏了,但是如果你在一个单例中注册视图信息,这将在整个应用程序中访问,你就错过了 MVC 的意义。视图由使用模型的控制器更新(并与之通信)。正如这里所说,这可能是对 Singleton 的误用,因此需要进行重构。

以上是关于关于设计模式:啥时候应该使用单例?的主要内容,如果未能解决你的问题,请参考以下文章

java 类中静态变量 和 单例模式下对象中的成员变量 有啥区别?使用场景是啥?

单例模式有啥缺点吗? [复制]

什么时候不应该使用单例模式? (除了显而易见的)

单例:应该如何使用

关于 std::cout,为啥使用“外部”而不是“单例模式”

是时候学习23种设计模式了-单例模式