单例模式(Singleton Pattern)学习笔记

单例模式(Singleton Pattern)学习笔记,同时包含在Unity中的应用

AssistedByChatGPT

前言

近期逐渐意识到要更进一步提升能力,要提高自己的姿势水平,还是得多学习一个,要非常熟悉Design pattern这一套,毕竟还是too young。(再也不能+1s了)

虽然有可能我在不了解Design pattern的情况下已经在使用了,但是和人交流的时候使用共通的专业用语可以大幅度提高沟通效率。比如在交流时说“噢,对于那个问题,只需使用单例(​Singleton​)就好”,每个人都会理解你建议的背后思想。如果大家都了解这个模式及其名称,就无需解释单例是什么。不然还要巴拉巴拉一通解释一通说,还不一定说得清楚。因此开始每几天学一个,用一个Design Pattern。

这是这套的第一篇,希望我能坚持下来吧。学习教材使用的是Refactoring.Guru,为了提高效率会和ChatGPT并用,大部分时间ChatGPT的解释是非常正确且详细的,所以我会在进行查阅和编辑后贴上,同时自己会再加上一些自己的理解和应用实例。如果有幸能在这段话里遇到你我还是推荐优先阅读原文Singleton,之后也可以看看本文的在Unity中Singleton的实例,原文真的写的非常浅显易懂了,你信我啊!

目的

单例是一种创建型设计模式(creational design pattern),它确保一个类只有一个实例,并提供对这个实例的全局访问点。

问题

单例模式同时解决了两个问题,但违反了单一职责原则(Single Responsibility Principle):

  1. 确保一个类只有单个实例:控制类实例的数量的常见原因是为了控制对某些共享资源的访问,比如数据库或文件。
  2. 提供对该实例的全局访问:单例模式允许从程序的任何地方访问某个对象,同时保护该实例不被其他代码覆写。

解决方案

单例的所有实现通常有两个共同点:

  • 将默认构造函数设为私有,防止使用new操作符与单例类。
  • 创建一个静态的创建方法,充当构造函数。在底层,此方法调用私有构造函数创建对象,并在静态字段中保存。此后对此方法的所有调用都将返回缓存的对象。

现实世界的类比

政府是单例模式的绝佳示例。一个国家只能有一个官方政府。不管组成政府的个人身份如何,“X国政府"这个名称是一个全球性的访问点,指代负责的那群人。

结构

  • 单例类声明了静态方法getInstance,返回其自身类的相同实例。
  • 单例的构造函数应对客户端代码隐藏。调用getInstance方法应是获取单例对象的唯一方式。

伪代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Database {
    private static field instance: Database;
    private constructor Database() { ... }

    public static method getInstance() {
        if (Database.instance == null) {
            acquireThreadLock() and then {
                if (Database.instance == null) {
                    Database.instance = new Database();
                }
            }
        }
        return Database.instance;
    }

    public method query(sql) { ... }
}

class Application {
    method main() {
        Database foo = Database.getInstance();
        foo.query("SELECT ...");
        Database bar = Database.getInstance();
        bar.query("SELECT ...");
        // 变量`bar`将包含与变量`foo`相同的对象。
    }
}

Singleton 模式的适用场景

1. 单例存在的需求

  • 当程序中的某个类只应该有一个实例,且这个实例需要被程序的不同部分共享时,应当使用Singleton(单例)模式。例如,一个被程序不同部分共享的单一数据库对象。

2. 创建对象的限制

  • Singleton模式禁用了除特殊创建方法外的所有其他创建类实例的手段。这个方法要么创建一个新对象,要么如果该对象已经被创建,则返回已存在的对象。

3. 对全局变量的控制

  • 使用Singleton模式当你需要对全局变量有更严格的控制。

4. 单例与全局变量的区别

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

5. 单例实例数量的调整

  • 注意,你总是可以调整这个限制,允许创建任意数量的Singleton实例。需要改变的唯一代码片段是getInstance方法的主体。

重要点

  • 单例模式的核心在于确保一个类仅有一个实例,并提供一个全局访问点
  • 与全局变量相比,单例模式提供了对实例化更精确的控制,尤其是在多线程环境中。
  • 在某些情况下,比如调整getInstance方法,可以灵活地改变单例模式的严格性,但这通常需要谨慎考虑。

如何实现

  1. 添加一个私有的静态字段,用于存储单例实例。
  2. 声明一个公共的静态创建方法,用于获取单例实例。
  3. 实现“懒加载”(lazy initialization),在静态方法内部在首次调用时创建新对象,并存放于静态字段中。该方法应始终在后续调用中返回该实例。
  4. 将类的构造函数设为私有。类的静态方法仍然可以调用构造函数,但其他对象不行。
  5. 修改客户端代码,用对静态创建方法的调用替换对单例构造函数的直接调用。

在Unity中Singleton的实例

创建一个Singleton的模版类,我所使用的Singleton模版类如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance;

    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = (T)FindObjectOfType(typeof(T));
                if (_instance == null)
                {
                    Debug.LogError("An instance of " + typeof(T) + " is needed in the scene, but there is none.");
                }
            }
            return _instance;
        }
    }

    public bool persistThroughScene = false;

    protected virtual void Awake()
    {
        if (_instance == null)
        {
            _instance = this as T;
            if (persistThroughScene)
            {
                DontDestroyOnLoad(gameObject);
            }
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

然后创建所要使用的Singleton并且继承这个模版。我在CommonParameterContainer中使用了Singleton用来存储单一Scene中的通用Parameter。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using System;
using UnityEngine;

public class CommonParameterContainer : Singleton<CommonParameterContainer>
{
    [Header("Env")]
    public bool lockMouse = false;
    public float damage = 50; // damage to enemy
    public float fireRate = 0.5f;
    public int timeLimit = 30;
    // etc...
    private void Start()
    {
        Instance.persistThroughScene = false;
        // etc...
    }
}

此时就可以在其他的Script中通过CommonParameterContainer.Instance.xxx;来调用Parameter,或者在script开头以commonParamCont = CommonParameterContainer.Instance;方式获取该单例的实例。以前总是用全局变量或者通过反复GetComponent的方式来调用Parameter,现在就优雅多了。

优点与缺点

  • 确保类只有一个实例。

  • 提供对该实例的全球性访问点。

  • 单例对象仅在首次请求时初始化。

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

  • 单例模式可能掩盖糟糕的设计,例如,程序组件过于相互了解。

  • 在多线程环境中需要特殊处理,以避免多个线程多次创建单例对象。

  • 单例的客户端代码可能难以单元测试。

与其他模式的关系

  • 外观模式(Facade):通常可以转换为单例,因为大多数情况下单个外观对象就足够了。
  • 享元模式(Flyweight):如果你以某种方式将所有共享状态的对象减少到一个享元对象,它就类似于单例。但两者有本质区别:单例只应有一个实例,而享元类可以有多个具有不同内在状态的实例;单例对象可以是可变的,享元对象是不可变的。
  • 抽象工厂(Abstract Factory)、建造者(Builder)、原型(Prototype)都可以实现为单例。
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus