523

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

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

4

22 回答 22

387

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

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

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

我敢肯定还有其他正当理由。Alex Miller 在“我讨厌的模式”中谈到服务定位器和客户端 UI 也可能是“可接受的”选择。

在 Singleton 阅读更多我爱你,但你让我失望了。

于 2008-10-23T02:53:19.093 回答
140

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

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

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

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

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

于 2008-10-23T01:59:20.967 回答
52

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

于 2008-10-23T01:31:52.583 回答
43

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

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

于 2008-10-23T01:21:32.307 回答
26

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

于 2008-10-23T01:35:04.197 回答
21

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

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

于 2008-10-23T01:19:35.663 回答
13

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

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

于 2008-10-23T02:05:51.017 回答
12

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

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

于 2008-10-23T01:26:20.720 回答
10

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

于 2011-05-23T20:19:51.767 回答
8

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

于 2009-11-30T16:30:54.273 回答
7

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

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

于 2008-12-21T00:10:03.623 回答
5

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

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

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

$gb->config['hostname']

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

$gb->lang['ENTER_USER']

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

$template

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

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

于 2010-11-02T20:32:06.993 回答
5

首先,让我们区分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,因为我们通过接口使用它。其中一项索赔消失了。让我们看看我们是否可以摆脱另一个声明 - 共享全局状态。

如果我们剥离 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;
    }
}

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

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

回顾:

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

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

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

于 2010-02-02T03:25:31.660 回答
3

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

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

于 2008-10-23T01:33:20.130 回答
2

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

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

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

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

于 2020-11-11T15:42:20.857 回答
1

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

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

于 2008-10-23T01:26:49.160 回答
1

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

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

于 2008-10-23T01:35:30.313 回答
1

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

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

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

于 2020-09-23T03:15:50.243 回答
0

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

于 2021-06-24T17:51:12.863 回答
0

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

于 2021-10-29T08:01:09.103 回答
-1

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

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

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

埃德。

于 2008-10-24T10:39:48.123 回答