14 Discuss the Design Patterns You Know

14 Discuss the design patterns you know #

Design patterns are reusable solutions abstracted from recurring problems in software development. To some extent, design patterns represent the best practices for specific situations and serve as a “jargon” for communication among software engineers. Understanding and mastering typical design patterns can help us improve communication, efficiency, and quality of our designs.

Today I want to ask you a question: Discuss the design patterns you know? Please manually implement the Singleton pattern, and mention which patterns are used in the Spring framework.

Typical Answer #

Design patterns can be classified into three categories based on their application goals: creational patterns, structural patterns, and behavioral patterns.

  • Creational patterns summarize various problems and solutions related to object creation. They include various factory patterns (Factory, Abstract Factory), singleton pattern, builder pattern, and prototype pattern.
  • Structural patterns summarize the practices related to software design structure, focusing on class and object inheritance, and composition methods. Common structural patterns include bridge pattern, adapter pattern, decorator pattern, proxy pattern, composite pattern, facade pattern, and flyweight pattern.
  • Behavioral patterns summarize patterns from the perspective of interaction between classes or objects and the division of responsibilities. Common behavioral patterns include strategy pattern, interpreter pattern, command pattern, observer pattern, iterator pattern, template method pattern, and visitor pattern.

Analysis of the Exam Points #

I suggest providing examples when answering to clarify what typical patterns look like and what typical use cases are. Here is an example from the Java Foundation Class Library for your reference. https://en.wikipedia.org/wiki/Design_Patterns.

First, in the 11th column, we just introduced the IO framework. We know that InputStream is an abstract class, and the standard library provides various subclasses such as FileInputStream and ByteArrayInputStream, which extend InputStream from different perspectives, making it a typical application of the decorator pattern.

To identify the decorator pattern, we can judge by recognizing the design characteristics of the class, that is, its class constructor takes the same abstract class or interface as an input parameter.

Because the decorator pattern essentially wraps instances of the same type, our invocation of the target object often goes through the methods overridden by the wrapper class to indirectly invoke the wrapped instance. This naturally achieves the purpose of adding additional logic, which is called “decoration.”

For example, BufferedInputStream adds caching to the input stream process through wrapping. Such decorators can also be nested multiple times to continuously add different levels of functionality.

public BufferedInputStream(InputStream in)

In the class diagram below, I briefly summarize the decorator pattern practice of InputStream.

Now let’s look at the second example. The creational pattern, especially the factory pattern, is commonly seen in our code. I’ll provide a relatively different API design practice. For example, in the latest version of the JDK, the HTTP/2 Client API, the process of creating HttpRequest is a typical builder pattern. It is usually implemented as an API in fluent style, which is also called method chaining by some people.

HttpRequest request = HttpRequest.newBuilder(new URI(uri))
                     .header(headerAlice, valueAlice)
                     .headers(headerBob, value1Bob,
                      headerCarl, valueCarl,
                      headerBob, value2Bob)
                     .GET()
                     .build();

Using the builder pattern can elegantly solve the complexities of building complex objects. Here, “complex” refers to a combination of multiple input parameters. If we use constructors, we often need to implement corresponding constructors for each possible combination of input parameters. A series of complex constructors will make the code difficult to read and maintain.

The analysis above also further reflects the original intention of the creational pattern, which is to abstract the object creation process separately, structurally separate the object usage logic from the object creation logic, hide the details of object instantiation, and provide users with a more standardized and unified logic.

When exploring design patterns in more depth, interviewers may:

  • Ask you to implement a typical design pattern. Although this seems simple, even the simplest Singleton pattern can comprehensively assess your basic coding skills.
  • Explore the use of typical design patterns, especially in combination with standard libraries or mainstream open-source frameworks, to assess your understanding of good industry practices.

If you happen to be asked about a pattern that you are not familiar with during an interview, you can give a slight hint, such as introducing a pattern that you are relatively familiar with and have used in your products, explaining what problem it solves, and discussing its advantages and disadvantages.

Next, I will analyze these two points in detail with code examples.

Knowledge Expansion #

Let’s implement a singleton design pattern that is very familiar in our daily lives. At first glance, the following example seems to meet the basic requirements:

public class Singleton {
   private static Singleton instance = new Singleton();
   public static Singleton getInstance() {
      return instance;
   }
}

Do you feel like something is missing? In fact, if a class does not explicitly declare a constructor, Java will automatically define a public parameterless constructor for it. Therefore, the example above cannot guarantee that additional objects will not be created. Others can simply call "new Singleton()". So how should we handle this?

Exactly, we can define a private constructor for the singleton (some suggest declaring it as an enum, which is controversial, and I personally don’t recommend choosing a relatively complex enum for daily development, after all, it’s not academic research). Is there any room for improvement?

In the previous column on ConcurrentHashMap, it was mentioned that lazy loading is used in many places in the standard library to improve initial memory overhead, which is also applicable to singletons. Here is an improved version after correction:

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

This implementation does not have any issues in a single-threaded environment. However, in a multi-threaded scenario, we need to consider thread safety. The most familiar approach is the “double-checked locking”, which includes the following points:

  • The use of volatile provides visibility and ensures that getInstance returns a fully initialized object.
  • Null check before syncrhonization is performed to avoid entering the relatively expensive synchronized block as much as possible.
  • Synchronization at the class level to ensure thread-safe method calls.
public class Singleton {
  private static volatile Singleton singleton = null;
  private Singleton() {
  }

  public static Singleton getSingleton() {
      if (singleton == null) { // Avoiding repeated entry into synchronized block
          synchronized (Singleton.class) { // Synchronizing on .class means synchronized class method calls
              if (singleton == null) {
                  singleton = new Singleton();
              }
          }
      }
      return singleton;
  }
}

In this code, there is much debate about using the volatile modifier on the static variable. When a Singleton class has multiple member variables, it needs to ensure that it is fully initialized before being accessed by get.

In modern Java, the memory model (JMM) is very complete. Through the volatile write or read, the so-called happen-before effect can be achieved, which avoids the frequently mentioned instruction reordering. In other words, the store instruction for constructing an object can be guaranteed to occur before the volatile read.

Of course, some people recommend using the approach of the inner class holding the static object. The theoretical basis for this approach is the implicit initialization lock during object initialization (if you are interested, you can refer to the explanation of LC in jls-12.4.2). Both this approach and the previous double-checked locking approach ensure thread safety, but the syntax is a bit obscure and may not have any particular advantages.

public class Singleton {
  private Singleton(){}
  public static Singleton getSingleton(){
      return Holder.singleton;
  }

  private static class Holder {
      private static Singleton singleton = new Singleton();
  }
}

As you can see, even the seemingly simplest singleton pattern requires a lot of considerations when adding various high standards requirements.

The above discussion has been quite academic. In practice, it may not be necessary to be so complex. If we look at the singleton implementation in Java core libraries, such as java.lang.Runtime, you will find that:

It does not use complex double-checked locking, etc.

The static instance is declared as final, which is often ignored in common practices. It to some extent ensures that the instance cannot be tampered with (as mentioned in Column 6, reflection and similar features can bypass private access restrictions), and it also has limited semantics for ensuring execution order.

private static final Runtime currentRuntime = new Runtime();
private static Version version;
// ...
public static Runtime getRuntime() {
  return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}

So far, we have looked at code practices in detail, but let’s briefly look at how mainstream open-source frameworks, such as Spring, use design patterns in API design. You should at least have a general understanding, such as:

  • BeanFactory and ApplicationContext apply the factory pattern.
  • In bean creation, Spring provides pattern implementations for different scopes, such as singleton and prototype.
  • In the AOP domain that I introduced in Column 6, it uses patterns such as the proxy pattern, decorator pattern, and adapter pattern.
  • Various event listeners are typical applications of the observer pattern.
  • Classes like JdbcTemplate apply the template pattern.

Today, I have reviewed the classification and main types of design patterns. From different perspectives such as Java core libraries and open-source frameworks, I have analyzed the pattern usage and different implementations of the singleton. I hope it can be helpful for your engineering practices. Finally, I would like to add that design patterns are not silver bullets, and it is important to avoid overusing or overdesigning them.

Exercise of the Day #

Have you mastered the knowledge of design patterns? Give it a thought. In business code, we often come across a large number of XXFacade. What problem does the facade pattern solve? When is it applicable?

Please share your thoughts in the comment section. I will select the most thoughtful comments and reward you with a study encouragement bonus. Feel free to discuss with me.

Are your friends also preparing for interviews? You can “Ask a Friend” by sharing today’s question with them. Maybe you can help them out.