singleton-pattern
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSingleton 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 on the exported instance to prevent accidental modifications
Object.freeze() - 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 class that has:
Counter- a method that returns the value of the instance
getInstance - a method that returns the current value of the
getCountvariablecounter - an method that increments the value of
incrementby onecounter - a method that decrements the value of
decrementby onecounter
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 class.
Counterjs
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()); // falseBy calling the method twice, we just set and equal to different instances. The values returned by the method on and effectively returned references to different instances: they aren't strictly equal!
newcounter1counter2getInstancecounter1counter2Let's make sure that only one instance of the class can be created.
CounterOne way to make sure that only one instance can be created, is by creating a variable called . In the constructor of , we can set equal to a reference to the instance when a new instance is created. We can prevent new instantiations by checking if the 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.
instanceCounterinstanceinstancejs
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 instance from the file. But before doing so, we should freeze the instance as well. The 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.
Countercounter.jsObject.freezejs
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 example with the following files:
Counter- : contains the
counter.jsclass, and exports aCounterinstance as its default exportCounter - : loads the
index.jsandredButton.jsmodulesblueButton.js - : imports
redButton.js, and addsCounter'sCountermethod as an event listener to the red button, and logs the current value ofincrementby invoking thecountermethodgetCount - : imports
blueButton.js, and addsCounter'sCountermethod as an event listener to the blue button, and logs the current value ofincrementby invoking thecountermethodgetCount
Both and import the same instance from . This instance is imported as in both files.
blueButton.jsredButton.jscounter.jsCounterWhen we invoke the method in either or , the value of the property on the 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.
incrementredButton.jsblueButton.jscounterCounter首先,我们来看一下使用ES2015类实现的单例是什么样的。在这个示例中,我们将构建一个类,它包含:
Counter- 方法:返回实例本身
getInstance - 方法:返回
getCount变量的当前值counter - 方法:将
increment的值加1counter - 方法:将
decrement的值减1counter
js
let counter = 0;
class Counter {
getInstance() {
return this;
}
getCount() {
return counter;
}
increment() {
return ++counter;
}
decrement() {
return --counter;
}
}不过,这个类并不符合单例的标准!单例应该只能被实例化一次。目前,我们可以创建多个类的实例。
Counterjs
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通过两次调用方法,我们创建了和两个不同的实例。和上的方法返回的是不同实例的引用:它们并不严格相等!
newcounter1counter2counter1counter2getInstance现在我们来确保类只能被实例化一次。
Counter一种实现方式是创建一个名为的变量。在的构造函数中,当创建新实例时,我们将设置为该实例的引用。我们可以通过检查变量是否已有值来阻止新的实例化。如果已有值,说明实例已经存在,这时应该抛出错误提醒用户。
instanceCounterinstanceinstancejs
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!完美!现在我们无法创建多个实例了。
接下来我们把实例从文件中导出。但在此之前,我们还应该冻结这个实例。方法可以确保使用该代码的用户无法修改单例。冻结后的实例无法添加或修改属性,这降低了意外覆盖单例值的风险。
Countercounter.jsObject.freezejs
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.jsredButton.jscounter.jsCounter无论我们在还是中调用方法,实例上的属性值都会在两个文件中同步更新。不管点击红色还是蓝色按钮,所有实例共享同一个值,所以计数器会持续加1,即使我们在不同文件中调用该方法。
redButton.jsblueButton.jsincrementCountercounterTradeoffs
优缺点
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 in the JavaScript example.
instanceHowever, 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 is simply an object containing:
counter- a property
count - an method that increments the value of
incrementby onecount - a method that decrements the value of
decrementby onecount
Since objects are passed by reference, both and are importing a reference to the same object. Modifying the value of in either of these files will modify the value on the , which is visible in both files.
redButton.jsblueButton.jscountercountcounter我们用之前的例子来演示。不过这次,只是一个包含以下内容的普通对象:
counter- 属性
count - 方法:将
increment的值加1count - 方法:将
decrement的值减1count
由于对象是按引用传递的,和导入的是同一个对象的引用。在任意一个文件中修改的值,都会修改上的值,且在两个文件中都能看到变化。
redButton.jsblueButton.jscountercountcounterTesting
测试
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, in this case, it may not be obvious that module is importing a Singleton. In other files, such as 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.jsindex.js当导入另一个模块(比如这里的)时,我们可能不会注意到该模块导入了一个单例。在其他文件中,比如这里的,我们可能会导入该模块并调用其方法,从而意外修改单例中的值。这可能会导致意外行为,因为单例的多个共享实例都会被修改。
superCounter.jsindex.jsGlobal 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 and keyword prevent developers from accidentally polluting the global scope, by keeping variables declared with these two keywords block-scoped. The new system in JavaScript makes creating globally accessible values easier without polluting the global scope, by being able to values from a module, and those values in other files.
letconstmoduleexportimportHowever, 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中,创建全局变量已经相当少见。新的和关键字通过将变量限制在块级作用域内,防止开发者意外污染全局作用域。JavaScript中的新模块系统通过允许从模块中值,并在其他文件中这些值,使得无需污染全局作用域就能创建全局可访问的值。
letconstexportimport不过,单例的常见用例是在应用中维护某种全局状态。让代码库的多个部分依赖同一个可变对象可能会导致意外行为。
通常,代码库的某些部分会修改全局状态中的值,而其他部分会使用这些数据。此时执行顺序非常重要:我们不希望在还没有数据的时候就去消费数据!随着应用的增长,当数十个组件相互依赖时,使用全局状态的数据流向会变得非常难以理解。
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
参考资料
- Do React Hooks replace Redux - Eric Elliott
- JavaScript Design Patterns: The Singleton - Samier Saeed
- Singleton - Refactoring Guru
- Do React Hooks replace Redux - Eric Elliott
- JavaScript Design Patterns: The Singleton - Samier Saeed
- Singleton - Refactoring Guru