Geek time-the beauty of design patterns. Singleton mode (middle): Why do I not recommend using singleton mode? What are the alternatives?

Although singleton is a very commonly used design pattern, we do use it often in actual development. However, some people think that singleton is an anti-pattern and it is not recommended. So, today, I will talk about these issues in detail in response to this statement: What are the problems with the singleton design pattern? Why is it called an anti-pattern? If you don't use a singleton, how to represent the globally unique class? What are the alternative solutions?

What are the problems with singletons?

In most cases, we use singletons in our projects, which are used to represent some globally unique classes, such as configuration information classes, connection pool classes, and ID generator classes. The singleton mode is simple to write and easy to use. In the code, we don't need to create an object, just call it directly through a method like IdGenerator.getInstance().getId(). However, this method of use is a bit similar to hard code, which brings many problems. Next, let's take a look at the specific problems.

1. Singleton is not friendly to OOP features

We know that the four major characteristics of OOP are encapsulation, abstraction, inheritance, and polymorphism. The singleton design pattern does not support abstraction, inheritance, and polymorphism. Why do you say that? We still use the example of IdGenerator to explain.


public class Order {
    
    
  public void create(...) {
    
    
    //...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

public class User {
    
    
  public void create(...) {
    
    
    // ...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

The use of IdGenerator violates the design principle based on interfaces rather than implementation, and also violates the abstract characteristics of OOP as understood in a broad sense. If one day in the future, we hope to adopt different ID generation algorithms for different businesses. For example, order ID and user ID are generated using different ID generators. In order to cope with this change in demand, we need to modify all the places where the IdGenerator class is used, so that the code changes will be relatively large.


public class Order {
    
    
  public void create(...) {
    
    
    //...
    long id = IdGenerator.getInstance().getId();
    // 需要将上面一行代码,替换为下面一行代码
    long id = OrderIdGenerator.getIntance().getId();
    //...
  }
}

public class User {
    
    
  public void create(...) {
    
    
    // ...
    long id = IdGenerator.getInstance().getId();
    // 需要将上面一行代码,替换为下面一行代码
    long id = UserIdGenerator.getIntance().getId();
  }
}

In addition, singletons are not friendly to inheritance and polymorphism. The reason why I use the word "unfriendly" instead of "completely unsupported" here is because theoretically, a singleton class can also be inherited and polymorphism can be achieved, but the implementation will be very strange. This leads to poor readability of the code. People who don't understand the design intent will find such a design inexplicable. Therefore, once you choose to design a class into a singleton class, it means giving up the two powerful object-oriented features of inheritance and polymorphism, which is equivalent to losing the scalability that can cope with future changes in requirements .

2. Singleton will hide dependencies between classes

We know that the readability of the code is very important. When reading the code, we hope to see the dependencies between classes at a glance and figure out which external classes this class depends on.

The dependencies between classes declared through constructors, parameter passing, etc. can be easily identified by looking at the definition of the function. However, the singleton class does not need to be explicitly created, and does not need to rely on parameter passing, and it can be called directly in the function. If the code is more complicated, this calling relationship will be very hidden. When reading the code, we need to carefully check the code implementation of each function to know which singleton classes this class relies on.

3. Singleton is not friendly to code scalability

We know that a singleton class can only have one object instance. If one day in the future, we need to create two or more instances in the code, then we need to make relatively big changes to the code. You might say, will there be such a demand? Since singleton classes are used to represent global classes in most cases, how come two or more instances are needed?

In fact, such demand is not uncommon. Let's take the database connection pool as an example to explain.

In the early stage of system design, we felt that there should only be one database connection pool in the system, so that we can control the consumption of database connection resources. Therefore, we designed the database connection pool class as a singleton class. But then we discovered that some SQL statements in the system run very slowly. When these SQL statements are executed, they occupy database connection resources for a long time, causing other SQL requests to fail to respond. In order to solve this problem, we hope to isolate slow SQL from other SQL for execution. In order to achieve this goal, we can create two database connection pools in the system. Slow SQL has an exclusive database connection pool, and other SQLs have an exclusive database connection pool. In this way, we can avoid slow SQL from affecting the execution of other SQLs.

If we design the database connection pool as a singleton class, obviously it will not be able to adapt to such changes in requirements, that is to say, the singleton class will affect the scalability and flexibility of the code in some cases. Therefore, it is best not to design resource pools such as database connection pools and thread pools as singletons. In fact, some open source database connection pools and thread pools are indeed not designed as singletons.

4. Singleton is not friendly to code testability

The use of singleton mode will affect the testability of the code. If the singleton class relies on a relatively heavy external resource, such as DB, when we write unit tests, we hope to replace it by mocking. The hard-coded use of singleton classes makes it impossible to implement mock replacement.

In addition, if the singleton class holds member variables (such as the id member variable in IdGenerator), it is actually equivalent to a kind of global variable, shared by all codes. If this global variable is a variable global variable, that is, its member variables can be modified, then when we write unit tests, we also need to pay attention to different test cases, modify the singleton class The value of the same member variable causes the problem of mutual influence of test results.

5. Singleton does not support parameterized constructor

Singletons do not support parameterized constructors. For example, when we create a singleton object of a connection pool, we cannot specify the size of the connection pool through parameters. In response to this problem, let's see what solutions are available.

The first solution is: after creating the instance, call the init() function to pass parameters. It should be noted that when we use this singleton class, we must call the init() method before calling the getInstance() method, otherwise the code will throw an exception. The specific code implementation is as follows:


public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton(int paramA, int paramB) {
    
    
    this.paramA = paramA;
    this.paramB = paramB;
  }

  public static Singleton getInstance() {
    
    
    if (instance == null) {
    
    
       throw new RuntimeException("Run init() first.");
    }
    return instance;
  }

  public synchronized static Singleton init(int paramA, int paramB) {
    
    
    if (instance != null){
    
    
       throw new RuntimeException("Singleton has been created!");
    }
    instance = new Singleton(paramA, paramB);
    return instance;
  }
}

Singleton.init(10, 50); // 先init,再使用
Singleton singleton = Singleton.getInstance();

The second solution is to put the parameters in the getIntance() method. The specific code implementation is as follows:


public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton(int paramA, int paramB) {
    
    
    this.paramA = paramA;
    this.paramB = paramB;
  }

  public synchronized static Singleton getInstance(int paramA, int paramB) {
    
    
    if (instance == null) {
    
    
      instance = new Singleton(paramA, paramB);
    }
    return instance;
  }
}

Singleton singleton = Singleton.getInstance(10, 50);

I don't know if you have noticed that the above code implementation is slightly problematic. If we execute the getInstance() method twice as follows, the obtained paramA and paramB of singleton1 and signleton2 are both 10 and 50. In other words, the second parameters (20, 30) did not work, and the construction process did not give a prompt, which would mislead users. How to solve this problem? Leave it to your own thinking, you can talk about your solution in the message area.


Singleton singleton1 = Singleton.getInstance(10, 50);
Singleton singleton2 = Singleton.getInstance(20, 30);

The third solution is to put the parameters in another global variable. The specific code implementation is as follows. Config is a global variable that stores the values ​​of paramA and paramB. The value inside can be defined by static constants like the following code, or it can be loaded from the configuration file. In fact, this method is the most recommended.


public class Config {
    
    
  public static final int PARAM_A = 123;
  public static final int PARAM_B = 245;
}

public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton() {
    
    
    this.paramA = Config.PARAM_A;
    this.paramB = Config.PARAM_B;
  }

  public synchronized static Singleton getInstance() {
    
    
    if (instance == null) {
    
    
      instance = new Singleton();
    }
    return instance;
  }
}

What are the alternative solutions?

We just mentioned a lot of problems with singletons. You might say that even if there are so many problems with singletons, I don’t have to. My business has a requirement for a globally unique class. If I don't use a singleton, how can I ensure that the objects of this class are globally unique?

In order to ensure global uniqueness, in addition to using singletons, we can also use static methods to achieve. This is also an implementation idea often used in project development. For example, the example of the unique ID increment generator mentioned in the previous lesson can be implemented with a static method, which looks like this:


// 静态方法实现方式
public class IdGenerator {
    
    
  private static AtomicLong id = new AtomicLong(0);
  
  public static long getId() {
    
     
    return id.incrementAndGet();
  }
}
// 使用举例
long id = IdGenerator.getId();

However, the realization of static methods does not solve the problems we mentioned earlier. In fact, it is more inflexible than singletons, for example, it cannot support lazy loading. Let's see if there is any other way. In fact, there is another way to use a singleton in addition to the way we used it before. The specific code is as follows:


// 1. 老的使用方式
public demofunction() {
    
    
  //...
  long id = IdGenerator.getInstance().getId();
  //...
}

// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {
    
    
  long id = idGenerator.getId();
}
// 外部调用demofunction()的时候,传入idGenerator
IdGenerator idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);

Based on the new usage, we pass the object generated by the singleton as a parameter to the function (also can be passed to the member variable of the class through the constructor), which can solve the problem of hidden dependencies between the singleton classes. However, other problems with singletons, such as unfriendly OOP features, scalability, and testability, cannot be solved.

Therefore, if we want to completely solve these problems, we may have to find other ways to implement globally unique classes from the root. In fact, the global uniqueness of class objects can be guaranteed in many different ways. We can either enforce the guarantee through the singleton model, or through the factory model, IOC container (such as Spring IOC container) to ensure that, but also through the programmers themselves (make sure not to create two classes when writing code Object). This is similar to the JVM responsible for the release of memory objects in Java, while the programmer is responsible for it in C++. The truth is the same.

For the detailed explanation of the alternative factory model and IOC container, we will explain it in the following chapters.

Guess you like

Origin blog.csdn.net/zhujiangtaotaise/article/details/110443138