1. Overview
1.概述
Singleton is one of the creational design patterns published by the Gang of Four in 1994.
Singleton 是四人帮于 1994 年发布的娱乐设计模式之一。
Because of its simple implementation, we tend to overuse it. Therefore, nowadays, it’s considered to be an anti-pattern. Before introducing it in our code, we should ask ourselves if we really need the functionality it provides.
由于其实现简单,我们往往会过度使用它。因此,如今它被认为是一种反模式。在代码中引入它之前,我们应该问问自己是否真的需要它所提供的功能。
In this tutorial, we’ll discuss the general drawbacks of the Singleton design pattern and see some alternatives we can use instead.
在本教程中,我们将讨论 Singleton 设计模式的一般缺点,并了解我们可以使用的一些替代方案。
2. Code Example
2.代码示例
First, let’s create a class we’ll use in our examples:
首先,让我们创建一个将在示例中使用的类:
public class Logger {
private static Logger instance;
private PrintWriter fileWriter;
public static Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
private Logger() {
try {
fileWriter = new PrintWriter(new FileWriter("app.log", true));
} catch (IOException e) {
e.printStackTrace();
}
}
public void log(String message) {
String log = String.format("[%s]- %s", LocalDateTime.now(), message);
fileWriter.println(log);
fileWriter.flush();
}
}
The class above represents a simplified class for logging into the file. We implemented it as a singleton, using the lazy initialization approach.
上面的类是一个用于登录文件的简化类。我们使用懒惰初始化方法将其作为单例实现。
3. Disadvantages of Singleton
3.单例的缺点
By definition, the Singleton pattern ensures a class has only one instance and, additionally, provides global access to this instance. Therefore, we should use it in cases where we need both of those things.
根据定义,Singleton 模式确保一个类只有一个实例,此外还提供对该实例的全局访问。因此,我们应该在需要这两样东西的情况下使用它。
Looking at its definition, we can notice it violates the Single Responsibility Principle. The principle states one class should have only one responsibility.
通过查看其定义,我们可以发现它违反了单一责任原则。该原则规定一个类只能有一个责任。
However, the Singleton pattern has at least two responsibilities – it ensures the class has only one instance and contains business logic.
不过,Singleton 模式至少有两个责任:确保类只有一个实例,并包含业务逻辑。
In the next sections, we’ll discuss some other pitfalls of this design pattern.
在接下来的章节中,我们将讨论这种设计模式的其他一些缺陷。
3.1. Global State
3.1.全球状态
We know global states are considered to be a bad practice and, thus, should be avoided.
我们知道,全局状态被认为是一种不好的做法,因此应该避免。
Although it may not be obvious, a singleton introduces global variables in our code, but they’re encapsulated within a class.
虽然可能不太明显,但单例在我们的代码中引入了全局变量,只不过它们被封装在一个类中。
Since they’re global, everyone can access and use them. Moreover, if they aren’t immutable, everyone can change them as well.
由于它们是全局的,因此每个人都可以访问和使用它们。此外,如果它们不是不可变的,那么每个人也都可以更改它们。
Suppose we use the Logger class in several places in our code. Everyone can access and modify its values.
假设我们在代码中的多个地方使用了 Logger 类。每个人都可以访问和修改它的值。
Now, if we encounter a problem in one method that uses it and discover the problem is in the singleton itself, we need to check the entire codebase and every method that uses it to find the impact of the problem.
现在,如果我们在使用单例的一个方法中遇到问题,并发现问题出在单例本身,我们就需要检查整个代码库和使用单例的每个方法,以找出问题的影响。
This can quickly become a bottleneck for our application.
这很快就会成为我们应用程序的瓶颈。
3.2. Code Flexibility
3.2.代码的灵活性
Next, in terms of software development, the only certainty lies in the fact our code will likely change in the future.
其次,在软件开发方面,唯一的确定性在于我们的代码将来可能会发生变化。
When a project is in the early stages of development, we can make the assumption there will be no more than one instance of certain classes and define them using the Singleton design pattern.
在项目开发的早期阶段,我们可以假设某些类的实例不会超过一个,并使用单例设计模式来定义它们。
However, if requirements change and our assumption turns out to be incorrect, we’d need to put a big effort into refactoring our code.
但是,如果需求发生变化,而我们的假设又不正确,那么我们就需要花大力气重构代码。
Let’s discuss the problem above in our working example.
让我们在工作示例中讨论上述问题。
We assumed we’d only need one instance of our Logger class. What if, in the future, we decide one file isn’t enough?
我们假设只需要一个 Logger 类的实例。如果将来我们觉得一个文件不够用怎么办?
For instance, we might need separate files for errors and info messages. Additionally, one instance of a class wouldn’t be enough anymore. Next, in order to make the modification possible, we’d need to refactor our entire codebase and remove the singleton, which would require a lot of effort.
例如,我们可能需要单独的错误和信息文件。此外,一个类的一个实例已经不够用了。接下来,为了使修改成为可能,我们需要重构整个代码库并移除单例,这将耗费大量精力。
With singletons, we’re making our code tightly coupled and less flexible.
如果使用单子,我们就会使代码紧密耦合,降低灵活性。
3.3. Dependency Hiding
3.3.依赖关系隐藏
Moving forward, singleton promotes hidden dependencies.
今后,单机版将推广隐藏的依赖关系。
To put it differently, when we’re using them in other classes, we’re hiding the fact these classes depend on a singleton instance.
换句话说,当我们在其他类中使用它们时,我们隐藏了这些类依赖于单例实例的事实。
Let’s consider the sum() method:
让我们来看看 sum() 方法:
public static int sum(int a, int b){
Logger logger = Logger.getInstance();
logger.log("A simple message");
return a + b;
}
If we don’t look directly at the implementation of the sum() method, we have no way of knowing it uses the Logger class.
如果我们不直接查看 sum() 方法的实现,就无法知道它使用了 Logger 类。
We didn’t pass the dependencies as usual, as arguments to the constructor or a method.
我们没有像往常一样,将依赖关系作为参数传递给构造函数或方法。
3.4. Multithreading
3.4.多线程
Next, in a multithreaded environment, singletons can be tricky to implement.
其次,在多线程环境中,单子的实现可能比较棘手。
The main problem is that the global variables are visible to all threads in our code. Moreover, each thread is unaware of the activities other threads make on the same instance.
主要问题在于全局变量对代码中的所有线程都是可见的。此外,每个线程都不知道其他线程在同一实例上的活动。
Therefore, we can end up facing different problems, such as race conditions and other synchronization issues.
因此,我们最终会面临不同的问题,例如 race conditions 和其他同步问题。
Our earlier implementation of the Logger class won’t work well in a multithreaded environment. Nothing in our method prevents multiple threads from accessing the getInstance() method at the same time. As a result, we can end up having more than one instance of the Logger class.
我们之前的 Logger 类实现在多线程环境中无法正常工作。我们的方法无法阻止多个线程同时访问 getInstance() 方法。因此,我们最终会拥有不止一个 Logger 类的实例。
Let’s modify the getInstance() method with the synchronized keyword:
让我们使用 synchronized 关键字修改 getInstance() 方法:
public static Logger getInstance() {
synchronized (Logger.class) {
if (instance == null) {
instance = new Logger();
}
}
return instance;
}
We’re now forcing every thread to wait its turn. However, we should be aware having synchronization is expensive. In addition, we are introducing an overhead to our method.
现在,我们迫使每个线程都等待轮到自己。不过,我们应该意识到同步的代价是昂贵的。此外,我们还为我们的方法引入了开销。
If necessary, one of the ways we can solve our problem is by applying the double-checking locking mechanism:
如有必要,我们可以采用 双重检查锁定机制来解决问题:
private static volatile Logger instance;
public static Logger getInstance() {
if (instance == null) {
synchronized (Logger.class) {
if (instance == null) {
instance = new Logger();
}
}
}
return instance;
}
However, we should keep in mind the JVM allows access to partially constructed objects, which may lead to unexpected behaviors of our program. Therefore, it’s required to add the volatile keyword to the instance variable.
不过,我们应牢记JVM允许访问部分构建的对象,这可能会导致程序出现意外行为。因此,需要在 instance 变量中添加 volatile 关键字。
Other alternatives we might consider include:
我们可以考虑的其他替代方案包括
- an eagerly created instance rather than a lazy one
- an Enum Singleton
- the Bill Pugh Singleton
3.5. Testing
3.5.测试
Going further, we can notice the downsides of a singleton when it comes to testing our code.
更进一步,我们可以发现单例在测试代码时的缺点。
Unit tests should test only a small portion of our code and shouldn’t depend on the other services that could fail, causing our test to fail as well.
单元测试应仅测试代码的一小部分,并且不应依赖于其他服务,因为这些服务可能会导致测试失败。
Let’s test our sum() method:
让我们测试一下 sum() 方法:
@Test
void givenTwoValues_whenSum_thenReturnCorrectResult() {
SingletonDemo singletonDemo = new SingletonDemo();
int result = singletonDemo.sum(12, 4);
assertEquals(16, result);
}
Even though our test passes, it creates a file with logs since the sum() method uses the Logger class.
尽管我们的测试通过了,但由于 sum() 方法使用了 Logger 类,它还是创建了一个包含日志的文件。
If something were wrong with our Logger class, our test would fail. Now, how should we prevent logging from happening?
如果我们的 Logger 类出了问题,我们的测试就会失败。现在,我们应该如何防止日志记录发生呢?
If applicable, one solution would be to mock the static getInstance() method using Mockito:
如果适用,一种解决方案是使用 Mockito 来模拟静态 getInstance() 方法:
@Test
void givenMockedLogger_whenSum_thenReturnCorrectResult() {
Logger logger = mock(Logger.class);
try (MockedStatic<Logger> loggerMockedStatic = mockStatic(Logger.class)) {
loggerMockedStatic.when(Logger::getInstance).thenReturn(logger);
doNothing().when(logger).log(any());
SingletonDemo singletonDemo = new SingletonDemo();
int result = singletonDemo.sum(12, 4);
Assertions.assertEquals(16, result);
}
}
4. Alternatives to Singleton
4.单人替代方案
Finally, let’s discuss some alternatives.
最后,让我们来讨论一些替代方案。
In cases where we need only one instance, we could use dependency injection. In other words, we can create only one instance and pass it as an argument where it’s needed. This way, we’d raise the awareness of dependencies a method or another class needs in order to function properly.
在只需要一个实例的情况下,我们可以使用依赖注入。 换句话说,我们可以只创建一个实例,并将其作为参数传递给需要它的地方。这样,我们就能提高对方法或其他类正常运行所需的依赖性的认识。
Additionally, if we need multiple instances in the future, we’d change our code more easily.
此外,如果我们将来需要多个实例,我们可以更方便地修改代码。
Moreover, we can use the Factory pattern for long-living objects.
此外,我们还可以使用工厂模式来创建长寿命对象。
5. Conclusion
5.结论
In this article, we looked at the main drawbacks of the Singleton design pattern.
在本文中,我们探讨了单例设计模式的主要缺点。
To sum up, we should use this pattern only when we really need it. Overusing it introduces unnecessary restrictions in cases where we don’t actually need a single instance. As an alternative, we can simply use dependency injection and pass the object as an argument.
总之,我们只有在真正需要的时候才能使用这种模式。在我们实际上并不需要单个实例的情况下,过度使用该模式会带来不必要的限制。作为替代方法,我们可以简单地使用依赖注入并将对象作为参数传递。
As always, the code of all examples is available over on GitHub.
一如既往,所有示例的代码均可在 GitHub 上 获取。