singleton-pattern

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Singleton Pattern

单例模式

Singletons are classes which can be instantiated once, and can be accessed globally. This single instance can be shared throughout our application, which makes Singletons great for managing global state in an application.
单例是只能实例化一次且可全局访问的类。这个单个实例可以在整个应用程序中共享,这使得单例非常适合管理应用中的全局状态。

When to Use

适用场景

  • Use this when you need exactly one instance of a class shared across the entire application
  • This is helpful for managing global state, configuration, or shared resources
  • 当你需要在整个应用中共享某个类的唯一实例时使用
  • 这有助于管理全局状态、配置或共享资源

Instructions

实现要点

  • Ensure only one instance can be created by checking for an existing instance in the constructor
  • Use
    Object.freeze()
    on the exported instance to prevent accidental modifications
  • In JavaScript, prefer simple object literals or modules over class-based singletons when possible
  • In React, prefer state management tools (Redux, Context) over Singletons for global state
  • Be aware that Singletons can make testing more difficult due to shared mutable state
  • 在构造函数中检查是否已存在实例,确保只能创建一个实例
  • 对导出的实例使用
    Object.freeze()
    ,防止意外修改
  • 在JavaScript中,可能的话优先使用简单对象字面量或模块,而非基于类的单例
  • 在React中,优先使用状态管理工具(Redux、Context)而非单例来管理全局状态
  • 注意:由于共享的可变状态,单例会使测试变得更困难

Details

详细实现

First, let's take a look at what a singleton can look like using an ES2015 class. For this example, we're going to build a
Counter
class that has:
  • a
    getInstance
    method that returns the value of the instance
  • a
    getCount
    method that returns the current value of the
    counter
    variable
  • an
    increment
    method that increments the value of
    counter
    by one
  • a
    decrement
    method that decrements the value of
    counter
    by one
js
let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}
However, this class doesn't meet the criteria for a Singleton! A Singleton should only be able to get instantiated once. Currently, we can create multiple instances of the
Counter
class.
js
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
By calling the
new
method twice, we just set
counter1
and
counter2
equal to different instances. The values returned by the
getInstance
method on
counter1
and
counter2
effectively returned references to different instances: they aren't strictly equal!
Let's make sure that only one instance of the
Counter
class can be created.
One way to make sure that only one instance can be created, is by creating a variable called
instance
. In the constructor of
Counter
, we can set
instance
equal to a reference to the instance when a new instance is created. We can prevent new instantiations by checking if the
instance
variable already had a value. If that's the case, an instance already exists. This shouldn't happen: an error should get thrown to let the user know.
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 counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!
Perfect! We aren't able to create multiple instances anymore.
Let's export the
Counter
instance from the
counter.js
file. But before doing so, we should freeze the instance as well. The
Object.freeze
method makes sure that consuming code cannot modify the Singleton. Properties on the frozen instance cannot be added or modified, which reduces the risk of accidentally overwriting the values on the Singleton.
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;
Consider an application that implements the
Counter
example with the following files:
  • counter.js
    : contains the
    Counter
    class, and exports a
    Counter
    instance
    as its default export
  • index.js
    : loads the
    redButton.js
    and
    blueButton.js
    modules
  • redButton.js
    : imports
    Counter
    , and adds
    Counter
    's
    increment
    method as an event listener to the red button, and logs the current value of
    counter
    by invoking the
    getCount
    method
  • blueButton.js
    : imports
    Counter
    , and adds
    Counter
    's
    increment
    method as an event listener to the blue button, and logs the current value of
    counter
    by invoking the
    getCount
    method
Both
blueButton.js
and
redButton.js
import the same instance from
counter.js
. This instance is imported as
Counter
in both files.
When we invoke the
increment
method in either
redButton.js
or
blueButton.js
, the value of the
counter
property on the
Counter
instance updates in both files. It doesn't matter whether we click on the red or blue button: the same value is shared among all instances. This is why the counter keeps incrementing by one, even though we're invoking the method in different files.
首先,我们来看一下使用ES2015类实现的单例是什么样的。在这个示例中,我们将构建一个
Counter
类,它包含:
  • getInstance
    方法:返回实例本身
  • getCount
    方法:返回
    counter
    变量的当前值
  • increment
    方法:将
    counter
    的值加1
  • decrement
    方法:将
    counter
    的值减1
js
let counter = 0;

class Counter {
  getInstance() {
    return this;
  }

  getCount() {
    return counter;
  }

  increment() {
    return ++counter;
  }

  decrement() {
    return --counter;
  }
}
不过,这个类并不符合单例的标准!单例应该只能被实例化一次。目前,我们可以创建多个
Counter
类的实例。
js
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
方法,我们创建了
counter1
counter2
两个不同的实例。
counter1
counter2
上的
getInstance
方法返回的是不同实例的引用:它们并不严格相等!
现在我们来确保
Counter
类只能被实例化一次
一种实现方式是创建一个名为
instance
的变量。在
Counter
的构造函数中,当创建新实例时,我们将
instance
设置为该实例的引用。我们可以通过检查
instance
变量是否已有值来阻止新的实例化。如果已有值,说明实例已经存在,这时应该抛出错误提醒用户。
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 counter1 = new Counter();
const counter2 = new Counter();
// Error: You can only create one instance!
完美!现在我们无法创建多个实例了。
接下来我们把
Counter
实例从
counter.js
文件中导出。但在此之前,我们还应该冻结这个实例。
Object.freeze
方法可以确保使用该代码的用户无法修改单例。冻结后的实例无法添加或修改属性,这降低了意外覆盖单例值的风险。
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;
我们来考虑一个实现了上述
Counter
示例的应用,它包含以下文件:
  • counter.js
    :包含
    Counter
    类,并默认导出一个**
    Counter
    实例**
  • index.js
    :加载
    redButton.js
    blueButton.js
    模块
  • redButton.js
    :导入
    Counter
    ,并将
    Counter
    increment
    方法作为事件监听器添加到红色按钮上,通过调用
    getCount
    方法记录
    counter
    的当前值
  • blueButton.js
    :导入
    Counter
    ,并将
    Counter
    increment
    方法作为事件监听器添加到蓝色按钮上,通过调用
    getCount
    方法记录
    counter
    的当前值
blueButton.js
redButton.js
导入的是
counter.js
中的同一个实例。这个实例在两个文件中都被称为**
Counter
**。
无论我们在
redButton.js
还是
blueButton.js
中调用
increment
方法,
Counter
实例上的
counter
属性值都会在两个文件中同步更新。不管点击红色还是蓝色按钮,所有实例共享同一个值,所以计数器会持续加1,即使我们在不同文件中调用该方法。

Tradeoffs

优缺点

Restricting the instantiation to just one instance could potentially save a lot of memory space. Instead of having to set up memory for a new instance each time, we only have to set up memory for that one instance, which is referenced throughout the application. However, Singletons are actually considered an anti-pattern, and can (or.. should) be avoided in JavaScript.
In many programming languages, such as Java or C++, it's not possible to directly create objects the way we can in JavaScript. In those object-oriented programming languages, we need to create a class, which creates an object. That created object has the value of the instance of the class, just like the value of
instance
in the JavaScript example.
However, the class implementation shown in the examples above is actually overkill. Since we can directly create objects in JavaScript, we can simply use a regular object to achieve the exact same result. Let's cover some of the disadvantages of using Singletons!
将实例化限制为唯一实例可能会节省大量内存空间。我们无需每次为新实例分配内存,只需为这个在整个应用中被引用的唯一实例分配一次内存。不过,单例实际上被认为是一种反模式,在JavaScript中应该尽量避免使用。
在许多编程语言中,比如Java或C++,我们无法像在JavaScript中那样直接创建对象。在这些面向对象编程语言中,我们需要先创建类,再通过类创建对象。创建的对象拥有该类实例的值,就像JavaScript示例中的
instance
值一样。
不过,上面示例中基于类的实现其实有些冗余。因为我们可以在JavaScript中直接创建对象,所以只需使用一个普通对象就能实现完全相同的效果。下面我们来看看使用单例的一些缺点!

Using a regular object

使用普通对象

Let's use the same example as we saw previously. However this time, the
counter
is simply an object containing:
  • a
    count
    property
  • an
    increment
    method that increments the value of
    count
    by one
  • a
    decrement
    method that decrements the value of
    count
    by one
Since objects are passed by reference, both
redButton.js
and
blueButton.js
are importing a reference to the same
counter
object. Modifying the value of
count
in either of these files will modify the value on the
counter
, which is visible in both files.
我们用之前的例子来演示。不过这次,
counter
只是一个包含以下内容的普通对象:
  • count
    属性
  • increment
    方法:将
    count
    的值加1
  • decrement
    方法:将
    count
    的值减1
由于对象是按引用传递的,
redButton.js
blueButton.js
导入的是同一个
counter
对象的引用。在任意一个文件中修改
count
的值,都会修改
counter
上的值,且在两个文件中都能看到变化。

Testing

测试

Testing code that relies on a Singleton can get tricky. Since we can't create new instances each time, all tests rely on the modification to the global instance of the previous test. The order of the tests matter in this case, and one small modification can lead to an entire test suite failing. After testing, we need to reset the entire instance in order to reset the modifications made by the tests.
依赖单例的代码测试会变得棘手。因为我们无法每次创建新实例,所有测试都依赖于前一个测试对全局实例的修改。此时测试的顺序至关重要,一个小小的修改可能会导致整个测试套件失败。测试完成后,我们需要重置整个实例,以清除测试所做的修改。

Dependency hiding

依赖隐藏

When importing another module,
superCounter.js
in this case, it may not be obvious that module is importing a Singleton. In other files, such as
index.js
in this case, we may be importing that module and invoke its methods. This way, we accidentally modify the values in the Singleton. This can lead to unexpected behavior, since multiple instances of the Singleton can be shared throughout the application, which would all get modified as well.
当导入另一个模块(比如这里的
superCounter.js
)时,我们可能不会注意到该模块导入了一个单例。在其他文件中,比如这里的
index.js
,我们可能会导入该模块并调用其方法,从而意外修改单例中的值。这可能会导致意外行为,因为单例的多个共享实例都会被修改。

Global behavior

全局行为

A Singleton instance should be able to get referenced throughout the entire app. Global variables essentially show the same behavior: since global variables are available on the global scope, we can access those variables throughout the application.
Having global variables is generally considered as a bad design decision. Global scope pollution can end up in accidentally overwriting the value of a global variable, which can lead to a lot of unexpected behavior.
In ES2015, creating global variables is fairly uncommon. The new
let
and
const
keyword prevent developers from accidentally polluting the global scope, by keeping variables declared with these two keywords block-scoped. The new
module
system in JavaScript makes creating globally accessible values easier without polluting the global scope, by being able to
export
values from a module, and
import
those values in other files.
However, the common usecase for a Singleton is to have some sort of global state throughout your application. Having multiple parts of your codebase rely on the same mutable object can lead to unexpected behavior.
Usually, certain parts of the codebase modify the values within global state, whereas others consume that data. The order of execution here is important: we don't want to accidentally consume data first, when there is no data to consume (yet)! Understanding the data flow when using a global state can get very tricky as your application grows, and dozens of components rely on each other.
单例实例应该能在整个应用中被引用。全局变量实际上表现出相同的行为:由于全局变量在全局作用域中可用,我们可以在整个应用中访问这些变量。
使用全局变量通常被认为是糟糕的设计决策。全局作用域污染可能会导致意外覆盖全局变量的值,进而引发许多意外行为。
在ES2015中,创建全局变量已经相当少见。新的
let
const
关键字通过将变量限制在块级作用域内,防止开发者意外污染全局作用域。JavaScript中的新模块系统通过允许从模块中
export
值,并在其他文件中
import
这些值,使得无需污染全局作用域就能创建全局可访问的值。
不过,单例的常见用例是在应用中维护某种全局状态。让代码库的多个部分依赖同一个可变对象可能会导致意外行为。
通常,代码库的某些部分会修改全局状态中的值,而其他部分会使用这些数据。此时执行顺序非常重要:我们不希望在还没有数据的时候就去消费数据!随着应用的增长,当数十个组件相互依赖时,使用全局状态的数据流向会变得非常难以理解。

State management in React

React中的状态管理

In React, we often rely on a global state through state management tools such as Redux or React Context instead of using Singletons. Although their global state behavior might seem similar to that of a Singleton, these tools provide a read-only state rather than the mutable state of the Singleton. When using Redux, only pure function reducers can update the state, after a component has sent an action through a dispatcher.
Although the downsides to having a global state don't magically disappear by using these tools, we can at least make sure that the global state is mutated the way we intend it, since components cannot update the state directly.
在React中,我们通常依赖Redux或React Context等状态管理工具来实现全局状态,而非使用单例。尽管它们的全局状态行为看起来与单例相似,但这些工具提供的是只读状态,而非单例的可变状态。使用Redux时,只有纯函数reducers可以更新状态,且需要组件通过dispatcher发送action后才能进行更新。
虽然使用这些工具并不能完全消除全局状态的缺点,但至少我们可以确保全局状态是按照预期的方式被修改的,因为组件无法直接更新状态。

Source

来源

References

参考资料