ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

G
Greg Bacon

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

在互联网上反复出现的一个原因是“日志记录”类(您提到过)。在这种情况下,可以使用 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 明确指定时,我无法理解为什么这是公认的答案:“给我一些场景,而不是使用单例的好旧记录器。”
m
metao

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

控制对共享资源的并发访问。

系统的多个不同部分将请求对资源的访问。

只能有一个对象。

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

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

一个简单的记录器是一个可能有效的单例的最明显的例子,但是这可以随着更复杂的记录方案而改变。


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

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


类似于 .NET 中的 Properties.Settings.Default
@Paul,“无单例阵营”将声明配置对象应该简单地传递给需要它的函数,而不是使其全局可访问(又名单例)。
不同意。如果将配置移到数据库中,一切都搞砸了。如果配置路径依赖于单例之外的任何东西,这些东西也需要是静态的。
@PaulCroarkin 您能否对此进行扩展并解释这是如何有益的?
@rr- 如果配置移动到数据库,它仍然可以封装在配置对象中,该对象将传递给需要它的函数。 (PS 我不在“非单身”阵营)。
V
Vincent Ramdhanie

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

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


我听说过这个打印机后台处理程序示例,我认为它有点蹩脚。谁说我不能拥有多个后台处理程序?到底什么是打印机假脱机程序?如果我有不同种类的打印机不能冲突或使用不同的驱动程序怎么办?
它只是一个示例……对于任何人用作示例的任何情况,您都可以找到使示例无用的替代设计。让我们假设假脱机程序管理由多个组件共享的单个资源。有用。
这是四人帮的经典例子。我认为一个真正经过验证的用例的答案会更有用。我的意思是您实际上认为 Singleton 是最佳解决方案的情况。
打印机后台处理程序到底是什么?
@1800INFORMATION 那么,经过这么多年,什么是打印机后台处理程序?..
M
Martin Beckett

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


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

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

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


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

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

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


D
Dave Markle

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

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


记录器通常是单例,因此不必传递记录对象。任何体面的日志流实现都将确保并发写入是不可能的,无论它是否是单例。
P
Peter Mortensen

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


D
Dean J

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


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

单例的一个实际示例可以在 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 历史记录和输出单例来发挥作用。

似乎在数据协调和行为灵活性之间存在推拉关系,可以通过将单例放在共享数据周围,尽可能减少行为以确保数据完整性,从而减轻这种影响。


s
sab

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

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

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

$gb->config['hostname']

或在数组中包含所有语言值,例如:

$gb->lang['ENTER_USER']

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

$template

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

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


您可能希望将答案分成多个段落并屏蔽代码段以提高可读性。
r
raiks

首先,让我们区分Single Object和Singleton。后者是前者的许多可能实现之一。而且 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,因为我们通过接口使用它。其中一项索赔消失了。让我们看看我们是否可以摆脱另一个声明 - 共享全局状态。

如果我们剥离 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模式 它有可以缓解的缺陷但是,Singleton的大部分复杂性都源于Single Object的复杂性无论您如何实例化Single Object,它仍然存在,无论是服务定位器、工厂方法还是其他东西 您可以将复杂性转移到经过(希望)良好测试的 DI 容器 有时使用 DI 容器很麻烦 - 想象为每个类注入一个 LOGGER


E
Emile Cormier

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

您还可以将 Abstract Factory 设为单例。


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

正如每个人所说,共享资源 - 特别是无法处理并发访问的东西。

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


L
Lakindu Hewawasam

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

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

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

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


C
Christo Kumar

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

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

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


T
Tanktalus

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

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


s
smaclell

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

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


B
Bennington

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


p
premganz

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


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

也许是一个带有代码的例子。

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

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

埃德。


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