Geek time - the beauty of design patterns - singleton mode (middle): why don't I recommend singleton mode? What are the alternatives?

Although singleton is a very common design pattern and we often use it in actual development, some people think that singleton is an anti pattern and it is not recommended. Therefore, today, I will talk about these questions in detail: what are the problems of the single example design mode? Why is it called anti pattern? How to represent a globally unique class without a singleton? What alternative solutions are there?

What are the problems with a single case?

In most cases, we use singleton in the project to represent some globally unique classes, such as configuration information class, connection pool class and ID generator class. The singleton mode is simple to write and easy to use. In the code, we do not need to create objects, but directly through similar idgenerator getInstance(). Just call a method like getid (). However, this method is a bit similar to hard code, which will bring many problems. Next, let's look at the specific problems.

1. The singleton is unfriendly to OOP feature support

As we know, the four characteristics of OOP are encapsulation, abstraction, inheritance and polymorphism. Singleton design pattern does not support abstraction, inheritance and polymorphism well. Why do you say that? Let's explain it through the example of IdGenerator.

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 interface rather than implementation, which violates the abstract characteristics of OOP 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 by different ID generators. In order to cope with this demand change, we need to modify all 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();
    // You need to replace the above line of code with the following line of code
    long id = OrderIdGenerator.getIntance().getId();
    //...
  }
}

public class User {
  public void create(...) {
    // ...
    long id = IdGenerator.getInstance().getId();
    // You need to replace the above line of code with the following line of code
    long id = UserIdGenerator.getIntance().getId();
  }
}

In addition, singleton support for inheritance and polymorphism is not friendly. The reason why I use the word "unfriendly" here rather than "completely unsupported" is that theoretically, singleton classes can also be inherited and polymorphic, but it will be very strange to implement, which will lead to poor readability of the code. People who do not understand the design intention will feel inexplicable when they see such a design. 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 demand changes.

2. Singleton will hide the 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 find out which external classes this class depends on.

The dependencies between classes declared by constructor and parameter passing can be easily identified by looking at the function definition. However, the singleton class does not need to be displayed and created, and does not need to rely on parameter passing. It can be called directly in the function. If the code is complex, this call 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 depends on.

3. The singleton is not friendly to the extensibility of the code

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, we need to make major changes to the code. You might say, will there be such a demand? Since singleton classes are mostly used to represent global classes, how can two or more instances be required?

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

In the early stage of system design, we think that there should only be one database connection pool in the system, which can facilitate us to control the consumption of database connection resources. Therefore, we design the database connection pool class as a singleton class. But then we found 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, resulting in other SQL requests unable to respond. To solve this problem, we want to isolate slow SQL from other SQL. In order to achieve this goal, we can create two database connection pools in the system. Slow SQL only enjoys one database connection pool and other SQL only enjoys another database connection pool. In this way, we can avoid slow SQL affecting the execution of other SQL.

If we design the database connection pool as a singleton class, it is obviously unable to adapt to such demand changes. In other words, the singleton class will affect the scalability and flexibility of the code in some cases. Therefore, resource pools such as database connection pool and thread pool should not be designed as singleton classes. In fact, some open source database connection pools and thread pools are not designed as singleton classes.

4. The singleton is not friendly to the testability of the code

The use of singleton mode will affect the testability of the code. If the singleton class depends on heavy external resources, such as DB, we hope to replace it by mock when writing unit tests. The hard coded use of singleton class makes it impossible to realize mock replacement.

In addition, if a singleton class holds a member variable (such as the id member variable in IdGenerator), it is actually equivalent to a global variable, which is shared by all code. If this global variable is a variable global variable, that is, its member variables can be modified, when writing unit tests, we also need to pay attention to the problem that the value of the same member variable in the singleton class is modified between different test cases, resulting in the interaction of test results.

5. The singleton does not support the constructor with parameters

Singleton does not support constructors with parameters. For example, we create a singleton object of connection pool. We can't specify the size of connection pool through parameters. To solve this problem, let's take a look at the solutions.

The first solution is to call init() function to pass parameters after creating the instance. It should be noted that when using this singleton class, we must call the init () method first, and then call 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 first, then use
Singleton singleton = Singleton.getInstance();

The second solution is to put the parameters into the getinterval () 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 found that there is a slight problem with the implementation of the above code. If we execute the getInstance() method twice as follows, the paramA and paramB of singleton1 and signleton2 are both 10 and 50. In other words, the second parameter (20, 30) does not work, and the construction process does not give a prompt, which will mislead the user. How to solve this problem? Leave it to you to think for yourself. You can talk about your solutions in the message area.

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

The third solution is to put the parameter into another global variable. The specific code implementation is as follows. Config is a global variable that stores paramA and paramB values. The values inside can be defined by static constants like the following code, or 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 alternative solutions are available?

Just now we mentioned many problems of single cases. You may say that even if there are so many problems in single cases, I don't have to. I have a business requirement to represent globally unique classes. How can I ensure that the objects of this class are globally unique without a singleton?

In order to ensure global uniqueness, in addition to using singletons, we can also use static methods. This is also an implementation idea often used in project development. For example, the example of ID unique increment generator mentioned in the last lesson is implemented by static method, which is as follows:

// Static method implementation
public class IdGenerator {
  private static AtomicLong id = new AtomicLong(0);
  
  public static long getId() { 
    return id.incrementAndGet();
  }
}
// Use examples
long id = IdGenerator.getId();

However, the implementation idea of static method can not solve the problems we mentioned earlier. In fact, it is more inflexible than singleton. For example, it cannot support delayed loading. Let's see if there is any other way. In fact, in addition to the use method we mentioned earlier, there is another use method for the single example. The specific codes are as follows:

// Old way of use 1
public demofunction() {
  //...
  long id = IdGenerator.getInstance().getId();
  //...
}

// 2. New usage: dependency injection
public demofunction(IdGenerator idGenerator) {
  long id = idGenerator.getId();
}
// When demofunction() is called externally, idGenerator is passed in
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 (or to the member variable of the class through the constructor), which can solve the problem of hiding the dependency between singleton classes. However, other problems of singleton, such as unfriendliness to 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 enforce the guarantee through singleton mode, factory mode and IOC container (such as Spring IOC container), and programmers themselves (they promise not to create two class objects when writing code). This is similar to the fact that the JVM is responsible for the release of memory objects in Java, while the programmer is responsible for the release of memory objects in C + +.

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

Posted by Judas on Wed, 04 May 2022 11:50:09 +0300