1 意图

单例模式可以保证一个类只有一个实例,提供一个访问该实例的全局节点。

singleton.png

2 问题

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

  1. 保证一个类只有一个实例。
    为什么要控制一个类的实例数量?最常见的原因是控制某些共享资源(如数据库或者文件)的访问权限。
    其运行方式是:如果创建了一个对象,稍后决定再创建一个对象时,会得到先前创建的对象,而不是一个新的对象。
    注意:普通构造函数无法实现上述行为,构造函数的设计决定了它必须总是返回一个新的对象。

singleton-comic-1-zh.png

  1. 为类的单一实例提供一个全局访问节点。
    全局变量使用十分方便,但是也非常不安全:任何代码都有可能覆盖掉全局变量的值,引发程序崩溃。
    单例模式允许程序在任何地方访问特定对象,同时可以保护该实例不被其他代码覆盖。
    此外,不希望解决同一个问题的代码分散在程序各处。更好的方式是将其放在同一个类中,特别是其他代码已经依赖这个类时更应该如此。

    3 解决方案

    单例模式包含两个步骤:
  • 将默认构造函数设置为私有,防止其他对象使用new运算符创建实例
  • 新建一个静态构建方法作为构造函数。该函数会调用私有构造函数来创建对象,将其保存在一个静态成员变量中。此后所有对该函数的调用都将返回这个缓存对象。

    4 结构

    structure-zh.png
    单例(Singleton)类声明了一个名为getInstace()的静态方法来返回所属类的一个相同实例。单例构造函数必须对客户端代码隐藏,调用getInstance()是获取单例对象的唯一方式。

    5 伪代码

    ``java // 数据库类会对getInstance(获取实例)`方法进行定义以让客户端在程序各处 // 都能访问相同的数据库连接实例。 class Database is // 保存单例实例的成员变量必须被声明为静态类型。 private static field instance: Database

    // 单例的构造函数必须永远是私有类型,以防止使用new运算符直接调用构 // 造方法。 private constructor Database() is

    1. // 部分初始化代码(例如到数据库服务器的实际连接)。
    2. // ...

    // 用于控制对单例实例的访问权限的静态方法。 public static method getInstance() is

    1. if (Database.instance == null) then
    2. acquireThreadLock() and then
    3. // 确保在该线程等待解锁时,其他线程没有初始化该实例。
    4. if (Database.instance == null) then
    5. Database.instance = new Database()
    6. return Database.instance

    // 最后,任何单例都必须定义一些可在其实例上执行的业务逻辑。 public method query(sql) is

    1. // 比如应用的所有数据库查询请求都需要通过该方法进行。因此,你可以
    2. // 在这里添加限流或缓冲逻辑。
    3. // ...

class Application is method main() is Database foo = Database.getInstance() foo.query(“SELECT …”) // … Database bar = Database.getInstance() bar.query(“SELECT …”) // 变量 barfoo 中将包含同一个对象。

  1. <a name="febb7d8d"></a>
  2. # 6 应用场景
  3. - **如果某个类对所有客户端只需要有一个可用的实例,可以使用单例模式。**<br />单例模式禁止通过特殊构建方法以外的任何方式来创建类的对象。该特殊方法可以创建新对象,如果该对象已经创建,则返回已有的对象。
  4. - **如果需要更加严格地控制全局变量,可以使用单例模式。**<br />单例模式与全局变量不同,它保证类只存在一个实例。除了类自身外,无法通过任何方式替换缓存的实例。<br />可以随时调整限制,设置生成实例的数量,只需要修改`getInstance()`方法。
  5. <a name="4170e445"></a>
  6. # 7 实现方式
  7. 1. 在类中添加私有静态成员变量用于保存单例实例。
  8. 2. 声明一个公有静态构建方法用于获取单例实例。
  9. 3. 在静态方法中实现“延迟初始化”。该方法会在首次被调用时创建一个对象,将其存储在静态成员变量中。此后该方法每次被调用时都返回该实例。
  10. 4. 将类的构造函数设置为私有。类的静态方法可以调用构造函数,其他对象不能调用。
  11. 5. 检查客户端代码,将对单例类构造函数的调用,替换成对其静态构建方法的调用。
  12. <a name="87338c0b"></a>
  13. # 8 优缺点
  14. - 优点
  15. - 可以保证一个类只有一个实例。
  16. - 有一个获取该实例的全局访问节点。
  17. - 仅在首次请求单例对象时进行初始化。
  18. - 缺点
  19. - 违反了单一职责原则,一个模式解决了两个问题(保证单个实例,获取该实例)。
  20. - 可能会掩盖不良设计,如程序组件之间相互了解过多。
  21. - 多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象。
  22. - 客户端代码的单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。单例类的构造方法是私有的,而且绝大部分语言无法重写静态方法,所以需要考虑模拟单例的方法。要么不编写测试代码,要么不使用单例模式。
  23. <a name="2b0a734f"></a>
  24. # 9 与其他模式的关系
  25. - [外观模式](https://refactoringguru.cn/design-patterns/facade)通常可以转换为[单例模式](https://refactoringguru.cn/design-patterns/singleton),因为大部分情况下一个外观对象就足够了。
  26. - 如果可以将所有共享状态简化为一个享元对象,则[享元模式](https://refactoringguru.cn/design-patterns/flyweight)和单例模拟类似了。但两个模式有根本的不同:
  27. - 单例类只有一个单例实体,但是享元类可以有多个实体,各实体的内在状态也可以不同。
  28. - 单例对象是可变的,享元对象是不可变的。
  29. - [抽象工厂模式](https://refactoringguru.cn/design-patterns/abstract-factory)、[生成器模式](https://refactoringguru.cn/design-patterns/builder)、[原型模式](https://refactoringguru.cn/design-patterns/prototype)都可以用单例模式实现。
  30. <a name="8b93ebe6"></a>
  31. # 10 代码示例
  32. <a name="a8920174"></a>
  33. ## 10.1 单例类
  34. ```go
  35. ackage main
  36. import (
  37. "fmt"
  38. "sync"
  39. )
  40. var lock = &sync.Mutex{}
  41. type single struct {
  42. }
  43. var singleInstance *single
  44. func getInstance() *single {
  45. if singleInstance == nil {
  46. lock.Lock()
  47. defer lock.Unlock()
  48. if singleInstance == nil {
  49. fmt.Println("Creating single instance now.")
  50. singleInstance = &single{}
  51. } else {
  52. fmt.Println("Single instance already created.")
  53. }
  54. } else {
  55. fmt.Println("Single instance already created.")
  56. }
  57. return singleInstance
  58. }

10.2 客户端

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. for i := 0; i < 30; i++ {
  7. go getInstance()
  8. }
  9. // Scanln is similar to Scan, but stops scanning at a newline and
  10. // after the final item there must be a newline or EOF.
  11. fmt.Scanln()
  12. }