设计模式之单例模式

设计模式是软件开发的基本组成部分,它为软件设计中常见的重复问题提供了典型的解决方案。设计模式并不是提供特定的软件,而只是用于以优化方式处理重复出现的主题的概念。其中,单例模式是一种常见且重要的设计模式,用于确保一个类只有一个实例,并提供对该实例的全局访问点。

什么是单例?

单例 是只可以实例化一次并可以全局访问的类。这个单一实例可以在我们的应用程序中共享,这使得单例模式非常适合管理应用程序中的全局状态。

如何构建一个单例类?

首先,让我们看一下在 ES2015 中类的单例是什么样子。对于这个例子,我们将构建一个 Counter 类,它具有以下的属性:

  • 一个返回当前实例的 getInstance 方法
  • 一个返回变量 counter 当前值的 getCount 方法
  • 一个将 counter 的值加一的 increment 方法
  • 一个将 counter 的值减一的 decrement 方法
javascript 复制代码
let counter = 0;

class Counter {
  getInstance() {
    return this;
  }
 
  getCount() {
    return counter;
  }
 
  increment() {
    return ++counter;
  }
 
  decrement() {
    return --counter;
  }
}

然而,这个类不符合 Singleton 的标准!Singleton 应该只能被实例化一次。目前,我们可以创建该类的多个实例 Counter

javascript 复制代码
const counter1 = new Counter();
const counter2 = new Counter();
 
console.log(counter1.getInstance() === counter2.getInstance()); // false

通过调用两次 new 方法,我们将会给 counter1counter2 赋予不同的实例。通过 getInstance 方法返回的 counter1counter2 的实例是对不同实例的引用:它们并不严格相等!

所以,我们需要确保 Counter只能创建唯一实例

为了确保只能创建唯一实例的方法,是创建一个 instance 的变量。在构造函数 Counter 中,我们可以在新实例创建的时候,设置为对 instance 的引用。通过检查 instance 是否已经有值的方式来防止发生新的实例化。如果已经有实例存在,我们应该抛出错误,让用户知道。

javascript 复制代码
let instance;
let counter = 0;
 
class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }
 
  getInstance() {
    return this;
  }
 
  getCount() {
    return counter;
  }
 
  increment() {
    return ++counter;
  }
 
  decrement() {
    return --counter;
  }
}
 
const counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!

这样修改之后,我们便无法创建第二个实例了。

下面,让我们把 Counter 实例从 counter.js 文件中导出。不过在此之前,我们应该先冻结该实力。使用 Object.freeze 方法可以确保对象无法被修改。无法对实例上的属性进行添加或者修改。这降低了单例的属性被意外覆盖等风险。

javascript 复制代码
const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

利弊

将实例化限制为仅可实例化一次可以节省大量内存空间。我们不必每次都为一个新实例设置内存,而只需为该实例设置一个内存即可,该实例在整个应用程序中都会被引用。然而,单例模式实际上被认为是一种反模式,并且可以(或..应该)避免在 JavaScript 中使用。

在许多编程语言中,例如 Java 或 C++,不可能像 JavaScript 那样直接创建对象。在那些面向对象的编程语言中,我们需要创建一个类,然后使用它再创建一个对象。创建的对象就是类的实例,就像 JavaScript 中的 instance 例子一样。

然而,上面示例中显示的类实现实际上是多余的。由于我们可以直接在 JavaScript 中创建对象,因此我们可以简单地使用常规对象来实现完全相同的结果。让我们来看看使用单例的一些缺点!

使用常规对象实现

让我们实现一个相同功能的示例。这一次,counter 是一个只包含以下内容的对象:

  • 一个 count 属性
  • 一个将 count 的值加一的 increment 方法
  • 一个将 count 的值减一的 decrement 方法
javascript 复制代码
let count = 0;

const counter = {
  increment() {
    return ++count;
  },
  decrement() {
    return --count;
  }
};

Object.freeze(counter);
export { counter };

由于对象是通过引用传递的,因此在外部导入时都是对同一个 counter 对象的引用。在任何文件中修改 count 的值都是修改 counter 中的值。

测试

测试依赖于单例的代码可能会很棘手。由于我们不能每次都创建新的实例,所以所有的测试都依赖于对上一次测试的全局实例的修改。在这种情况下,测试的顺序就变得很重要,一个小小的修改可能会导致整个测试流程失败。测试完成后,我们还需要重置整个实例,以重置测试所做的修改。

全局变量

单例模式下实例应该能够在整个应用程序中被引用。全局变量本质上表现出相同的行为:由于全局变量在全局范围内可用,因此我们可以在整个应用程序中访问这些变量。

拥有全局变量通常被认为是一个糟糕的设计决策。全局范围污染最终可能会意外覆盖全局变量的值,这可能会导致许多意外行为。

在 ES2015 中,创建全局变量相当罕见。新的 letconst 通过将使用这两个关键字声明的变量保留在块范围内,可以防止开发人员意外污染全局范围。JavaScript 中的新 module 系统可以更轻松地创建全局可访问的值,而不会污染全局范围,因为能够 export 从模块中获取值以及 import 其他文件中的值。

然而,单例的常见用例是在整个应用程序中拥有某种全局状态。让代码库的多个部分,依赖于同一个可变对象可能会导致意外的行为。

通常,代码库的某些部分会修改全局状态中的值,而其他部分则使用该数据。这里的执行顺序很重要:我们不想在没有数据可供使用时意外地首先使用数据!随着应用程序的增长,并且数十个组件相互依赖,理解使用全局状态时的数据流可能会变得非常棘手。

总结

单例模式作为一种重要的设计模式,在软件开发中扮演着重要的角色。通过深入理解单例模式的概念、实现方式和应用场景,我们可以更好地应用它来解决实际的设计问题。同时,我们也要注意单例模式的使用时机和限制,避免滥用导致不必要的复杂性。

相关推荐
敲代码的 蜡笔小新25 分钟前
【行为型之观察者模式】游戏开发实战——Unity事件驱动架构的核心实现策略
观察者模式·unity·设计模式·c#
运维@小兵34 分钟前
vue使用路由技术实现登录成功后跳转到首页
前端·javascript·vue.js
肠胃炎36 分钟前
React构建组件
前端·javascript·react.js
邝邝邝邝丹42 分钟前
React学习———React.memo、useMemo和useCallback
javascript·学习·react.js
美酒没故事°1 小时前
纯css实现蜂窝效果
前端·javascript·css
GISer_Jing2 小时前
React useState 的同步/异步行为及设计原理解析
前端·javascript·react.js
mini榴莲炸弹2 小时前
什么是SparkONYarn模式?
前端·javascript·ajax
能来帮帮蒟蒻吗2 小时前
VUE3 -综合实践(Mock+Axios+ElementPlus)
前端·javascript·vue.js·笔记·学习·ajax·typescript
啊啊啊~~2 小时前
歌词滚动效果
javascript·html
球球和皮皮3 小时前
Babylon.js学习之路《四、Babylon.js 中的相机(Camera)与视角控制》
javascript·3d·前端框架·babylon.js