设计模式 - 单例模式模式

接下来会有一个关于设计模式的专栏 有兴趣的可以插个眼👀

原文地址 www.patterns.dev/posts/singl...

预告: 下一篇代理模式

设计模式是软件开发的基本组成部分,因为它们为软件设计中经常出现的问题提供了典型的解决方案。设计模式不是提供特定的软件,而只是可用于以优化方式处理重复主题的概念。

在过去的几年里,Web开发生态系统发生了迅速的变化。虽然一些众所周知的设计模式可能根本不像以前那样有价值,但其他设计模式已经发展到使用最新技术解决现代问题。

Facebook的JavaScript库React在过去5年中获得了巨大的吸引力,与AngularVueEmberSvelte等竞争JavaScript库相比,目前是** NPM **上下载最频繁的框架。由于 React 的普及,设计模式已被修改、优化并创建新的模式,以便在当前的现代 Web 开发生态系统中提供价值。最新版本的 React 引入了一个名为 Hooks 的新功能,它在你的应用程序设计中起着非常重要的作用,可以取代许多传统的设计模式。

现代 Web 开发涉及许多不同类型的模式。本项目涵盖了使用 ES2015+ 的常见设计模式的实现、优势和陷阱、特定于 React 的设计模式及其使用 React Hooks 的可能修改和实现,以及更多有助于改进现代 Web 应用程序的模式和优化!

Singleton Pattern 单例模式

首先,让我们看一下使用 ES2015 类的单例会是什么样子。对于此示例,我们将构建一个 Counter 具有以下功能的类:

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

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}

但是,此类不符合单例的标准!单例应该只能实例化一次。目前,我们可以创建 Counter 类的多个实例。

javascript 复制代码
let counter = 0;
 
class Counter {
  getInstance() {
    return this;
  }
 
  getCount() {
    return counter;
  }
 
  increment() {
    return ++counter;
  }
 
  decrement() {
    return --counter;
  }
}
 
const counter1 = new Counter();
const counter2 = new Counter();
 
console.log(counter1.getInstance() === counter2.getInstance()); // false

通过调用 new 该方法两次,我们只是设置 counter1counter2 等于不同的实例。 getInstance 该方法 counter1 counter2 返回的值有效地返回了对不同实例的引用:它们并不严格相等! 所以我们需要确保只能创建 Counter 类的一个实例。

确保只能创建一个实例的一种方法是创建一个名为 instance 的变量。在Counter的构造函数中,我们可以在创建新实例时设置为 instance 对实例的 Counter 引用。我们可以通过检查 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.js 文件中导出 Counter 实例。但在此之前,我们还应该冻结实例。该方法 Object.freeze 确保使用代码无法修改单一实例。无法添加或修改冻结实例上的属性,这降低了意外覆盖单一实例上的值的风险。

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 singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

让我们看一下实现该 Counter 示例的应用程序。我们有以下文件:

  • counter.js :包含 Counter 类,并将 Counter 实例导出为其默认导出
  • index.js :加载 redButton.jsblueButton.js 模块
  • redButton.js :导入 Counter ,并将 Counter 的方法 increment 作为事件侦听器添加到红色按钮,并通过调用 getCount 该方法记录 的 counter 当前值
  • blueButton.js :导入 Counter ,并将 Counter 的方法 increment 作为事件侦听器添加到蓝色按钮,并通过调用 getCount 该方法记录 的 counter 当前值
arduino 复制代码
// index.js

import "./redButton";
import "./blueButton";

console.log("Click on either of the buttons 🚀!");
javascript 复制代码
// counter.js

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 singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

blueButton.jsredButton.jscounter.js 导入相同的Counter实例

当我们在 blueButton.js或者 redButton.js 中调用 increment 该方法时, Counter 实例上的 counter 属性值会在两个文件中更新并共享该值。无论我们单击红色还是蓝色按钮计数器不断递增 1 ,即使我们在不同的文件中调用该方法。

权衡利弊

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

在许多编程语言中,例如Java或C++,不可能像在JavaScript中那样直接创建对象。在那些面向对象的编程语言中,我们需要创建一个类,由该类创建一个对象。创建的对象具有类实例的值,就像 JavaScript 示例中的值 instance 一样。

但是,上面示例中所示的类实现实际上是矫枉过正的。由于我们可以直接在 JavaScript 中创建对象,因此我们可以简单地使用常规对象来实现完全相同的结果。让我们介绍一下使用单例的一些缺点!

使用常规对象

让我们使用之前看到的相同示例。但是,这一次,counter 只是一个包含以下内容的对象:

  • 一个 count 属性
  • 方法 incrementcount值递增 1
  • 方法 decrementcount值递减 1
ini 复制代码
let count = 0;

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

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

由于对象是通过引用传递的,因此redButton.jsblueButton.js 都在导入counter 对象的引用。修改其中任一文件中的值 count 将修改 counter 的值,该值在两个文件中都可见。

测试

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

Dependency hiding 依赖隐藏

在这种情况下,导入另一个模块时, superCounter.js 模块正在导入单例可能并不明显。在其他文件中,例如 index.js 在这种情况下,我们可能会导入该模块并调用其方法。这样,我们意外地修改了单例中的值。这可能会导致意外行为,因为可以在整个应用程序中共享 Singleton 的多个实例,这些实例也会被修改。

scss 复制代码
// test.js
import Counter from "../src/counterTest";

test("incrementing 1 time should be 1", () => {
  Counter.increment();
  expect(Counter.getCount()).toBe(1);
});

test("incrementing 3 extra times should be 4", () => {
  Counter.increment();
  Counter.increment();
  Counter.increment();
  expect(Counter.getCount()).toBe(4);
});

test("decrementing 1  times should be 3", () => {
  Counter.decrement();
  expect(Counter.getCount()).toBe(3);
});
javascript 复制代码
// superCounter.js
import Counter from "./counter";

export default class SuperCounter {
  constructor() {
    this.count = 0;
  }

  increment() {
    Counter.increment();
    return (this.count += 100);
  }

  decrement() {
    Counter.decrement();
    return (this.count -= 100);
  }
}

全局行为

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

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

在ES2015中,创建全局变量相当罕见。new let and const 关键字通过保持使用这两个关键字声明的变量在块范围内来防止开发人员意外污染全局范围。JavaScript 中的新 module 系统通过能够从 export 模块中获取值以及其他 import 文件中的值,使创建全局可访问的值变得更加容易,而不会污染全局范围。

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

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

React 中的状态管理

在 React 中,我们经常通过ReduxReact Context 等状态管理工具依赖全局状态,而不是使用 Singletons。尽管它们的全局状态行为可能看起来与单一实例的行为相似,但这些工具提供只读状态 ,而不是单一实例的可变 状态。使用 Redux 时,在组件通过dispatcher 发送action 后,只有纯函数reducers才能更新状态。

尽管使用这些工具不会神奇地消失全局状态的缺点,但我们至少可以确保全局状态按照我们预期的方式发生变异,因为组件不能直接更新状态。

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax