编程

设计模式之单例(Singleton)模式

731 2024-02-04 01:31:00

意图

单例(Singleton)模式一种创建型的设计模式,可以确保类只有一个实例,同时为该实例提供全局访问点。

问题描述

单例模式同时解决了两个问题,违反了单一职责原则。

确保一个类只有一个实例。为什么有人要控制一个类由多少各实例呢?最常见的原因是控制对某些共享资源的访问,例如数据库或文件。

它的工作原理如下:想象一下你创建了一个对象,但过了一段时间后决定创建一个新对象,你将获得已创建的对象,而不是接收新对象。

请注意,这种行为是不可能用常规构造函数实现的,因为构造函数调用必须始终按设计返回一个新对象。

客户端甚至没有意识到它一直在与同一个对象协作。

请注意,这种行为是不可能用常规构造函数实现的,因为构造函数调用必须始终按设计返回一个新对象。

提供该实例的全局访问点。还记得用来存储一些基本对象的那些全局变量吗?虽然它们非常方便,但也非常不安全,因为任何代码都可能覆盖这些变量的内容并使应用程序崩溃。

就像全局变量一样,单例(Singleton)模式允许从程序中的任何位置访问某些对象。但是,它也可以保护该实例不被其他代码覆盖。

这个问题还有另一面:你不希望解决问题 #1 的代码分散在你的程序中的各个地方。将它放在一个类中要好得多,尤其是如果代码的其余部分已经依赖于它的话。

如今,单例(Singleton)模式已经变得如此流行,以至于人们可能会将一些即使只解决列出的问题之一也称之9为单例(Singleton)

方案

单例(Singleton)的所有实现都有以下两个共同步骤:

  • 将默认构造函数设为私有构造函数,以防止其他对象将 new 运算符调用单例类。
  • 创建一个充当构造函数的静态创建方法。在后台,此方法调用私有构造函数来创建对象并将其保存在静态字段中。接下来对该方法的所有调用都返回缓存的对象。

如果您的代码可以访问单例(Singleton)类,那么它就可以调用单例(Singleton)的静态方法。因此,每当调用该方法时,总是返回相同的对象。

真实世界类比

政府是单例模式的一个很好的例子。一个国家只能有一个官方政府。无论组建政府的个人的个人身份如何,“X 政府”这一名称都是一个全球接入点,用于识别负责人群体。

结构

单例(Singleton)类声明了静态方法 getInstance,该方法返回自身类的同一个实例。

单例(Singleton)的构造函数应该对客户端代码隐藏。调用 getInstance 方法应该是获取单例(Singleton)对象的唯一方法。

伪代码

本例中,数据库链接扮演者一个单例(Singleton)。该类没有公开的构造函数,因此获取其对象的唯一方式是调用 getInstance 方法。该方法缓存了第一次创建的对象并将其返回给后续的调用。

// The Database class defines the `getInstance` method that lets
// clients access the same instance of a database connection
// throughout the program.
class Database is
    // The field for storing the singleton instance should be
    // declared static.
    private static field instance: Database
    // The singleton's constructor should always be private to
    // prevent direct construction calls with the `new`
    // operator.
    private constructor Database() is
        // Some initialization code, such as the actual
        // connection to a database server.
        // ...
    // The static method that controls access to the singleton
    // instance.
    public static method getInstance() is
        if (Database.instance == null) then
            acquireThreadLock() and then
                // Ensure that the instance hasn't yet been
                // initialized by another thread while this one
                // has been waiting for the lock's release.
                if (Database.instance == null) then
                    Database.instance = new Database()
        return Database.instance
    // Finally, any singleton should define some business logic
    // which can be executed on its instance.
    public method query(sql) is
        // For instance, all database queries of an app go
        // through this method. Therefore, you can place
        // throttling or caching logic here.
        // ...
class Application is
    method main() is
        Database foo = Database.getInstance()
        foo.query("SELECT ...")
        // ...
        Database bar = Database.getInstance()
        bar.query("SELECT ...")
        // The variable `bar` will contain the same object as
        // the variable `foo`.

适用场景

当程序中的某个类只应该一个实例对所有客户端可用时,比如,程序多个地方分享的一个数据库对象,可以使用单例模式。

单例(Singleton)模式禁用了除特殊创建方法之外的所有其他创建类对象的方法。该方法要么创建一个新对象,要么返回一个已经创建的现有对象。

当需要对全局变量进行更严格的控制时,请使用单例模式。

与全局变量不同,单例(Singleton)模式保证一个类只有一个实例。除了单例(Singleton)类本身之外,没有任何东西可以替换缓存的实例。

请注意,你可以随时调整此限制,并允许创建任意数量的单例(Singleton)实例。唯一需要更改的代码是 getInstance 方法的主体。

如何实现

在类中添加一个私有的静态字段,用以存储单例实例。

声明一个共有的静态创建方法,以获取单例实例。

在静态方法内实现延迟初始化。第一次调用时创建一个新对象并将其放到静态字段中。后续的调用中该方法应该总是返回那个实例。

使构造函数成为私有。类的静态函数仍然可以调用构造函数,但其他对象不行。

仔细检查客户端代码,将对单例构造函数的所有直接调用替换为对其静态创建方法的调用。

优缺点

  • ✔️可以确保该类只有一个实例。
  • ✔️你得到了该实例的全局访问点。
  • ✔️单例对象只会在第一次请求的时候初始化。
  • ❌违反了单一职责原则。该模式同时解决了两个问题。

❌单例模式可能掩盖了糟糕的设计,例如,当程序的组件之间彼此了解太多等。

❌该模式在多线程环境中需要进行特殊处理,这样多个线程才不会多次创建单例对象。

❌对但单例的客户端代码进行单元测试可能很难,因为许多测试框架在生成模拟对象时依赖于继承。由于单例类的构造函数是私有的,并且在大多数语言中不可能覆盖静态方法,因此需要想出一种创造性的方法来模拟单例。或者就是不写测试。或者不要使用单例模式。

与其他模式的关系

门面(Facade)类可以转换成单例(Singleton), 因为在大多数情况下,一个 Facade 对象就足够了。

如果设法将私有共享状态的对象减少为一个享元(Flyweight)对象,那么享元(Flyweight)模式将与单例(Singleton)相似。不过这两个模式由两个根本区别:

  1. 单例只能由一个实例,而 一个 Flyweight 类可以有多个具有不同内部状态的实例。
  2. 单例(Singleton)对象可以是可变的。Flyweight 对象是不可变的。

抽象工厂(Abstract Factory)、建造者(Builder)原型(Prototype)可以使用单例(Singleton)实现。

代码示例

index.php: 概念示例

<?php
namespace RefactoringGuru\Singleton\Conceptual;
/**
 * The Singleton class defines the `GetInstance` method that serves as an
 * alternative to constructor and lets clients access the same instance of this
 * class over and over.
 */
class Singleton
{
    /**
     * The Singleton's instance is stored in a static field. This field is an
     * array, because we'll allow our Singleton to have subclasses. Each item in
     * this array will be an instance of a specific Singleton's subclass. You'll
     * see how this works in a moment.
     */
    private static $instances = [];
    /**
     * The Singleton's constructor should always be private to prevent direct
     * construction calls with the `new` operator.
     */
    protected function __construct() { }
    /**
     * Singletons should not be cloneable.
     */
    protected function __clone() { }
    /**
     * Singletons should not be restorable from strings.
     */
    public function __wakeup()
    {
        throw new \Exception("Cannot unserialize a singleton.");
    }
    /**
     * This is the static method that controls the access to the singleton
     * instance. On the first run, it creates a singleton object and places it
     * into the static field. On subsequent runs, it returns the client existing
     * object stored in the static field.
     *
     * This implementation lets you subclass the Singleton class while keeping
     * just one instance of each subclass around.
     */
    public static function getInstance(): Singleton
    {
        $cls = static::class;
        if (!isset(self::$instances[$cls])) {
            self::$instances[$cls] = new static();
        }
        return self::$instances[$cls];
    }
    /**
     * Finally, any singleton should define some business logic, which can be
     * executed on its instance.
     */
    public function someBusinessLogic()
    {
        // ...
    }
}
/**
 * The client code.
 */
function clientCode()
{
    $s1 = Singleton::getInstance();
    $s2 = Singleton::getInstance();
    if ($s1 === $s2) {
        echo "Singleton works, both variables contain the same instance.";
    } else {
        echo "Singleton failed, variables contain different instances.";
    }
}
clientCode();

Output.txt: 执行结果

Singleton works, both variables contain the same instance.