Geek time-the beauty of design patterns Builder mode: Detailed explanation of three object creation methods: constructor, set method, and builder mode

Builder mode, Chinese translated as builder mode or builder mode, some people call it builder mode.

In fact, the principle and code implementation of the builder pattern are very simple, and it is not difficult to master. The difficulty lies in the application scenarios. For example, have you considered the following questions: You can create objects by directly using the constructor or with the set method. Why do you need the builder mode to create it? Both the builder mode and the factory mode can create objects, so what is the difference between the two?

Why is the builder mode needed?

In normal development, the most common way to create an object is to use the new keyword to call the constructor of the class. My question is, under what circumstances is this method not applicable, and the builder pattern is needed to create objects? You can think about it first. Let me show you through an example.

Suppose there is such a design interview question: we need to define a resource pool configuration class ResourcePoolConfig. The resource pool here can be simply understood as thread pool, connection pool, object pool, etc. In this resource pool configuration class, there are the following member variables, which are configurable items. Now, please write code to implement this ResourcePoolConfig class.

Insert picture description here
As long as you have a little development experience, it is not difficult for you to implement such a class. The most common and easy to think of implementation ideas are shown in the following code. Because maxTotal, maxIdle, and minIdle are not required variables, when creating the ResourcePoolConfig object, we pass null values ​​to these parameters in the constructor to indicate the use of default values.


public class ResourcePoolConfig {
    
    
  private static final int DEFAULT_MAX_TOTAL = 8;
  private static final int DEFAULT_MAX_IDLE = 8;
  private static final int DEFAULT_MIN_IDLE = 0;

  private String name;
  private int maxTotal = DEFAULT_MAX_TOTAL;
  private int maxIdle = DEFAULT_MAX_IDLE;
  private int minIdle = DEFAULT_MIN_IDLE;

  public ResourcePoolConfig(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {
    
    
    if (StringUtils.isBlank(name)) {
    
    
      throw new IllegalArgumentException("name should not be empty.");
    }
    this.name = name;

    if (maxTotal != null) {
    
    
      if (maxTotal <= 0) {
    
    
        throw new IllegalArgumentException("maxTotal should be positive.");
      }
      this.maxTotal = maxTotal;
    }

    if (maxIdle != null) {
    
    
      if (maxIdle < 0) {
    
    
        throw new IllegalArgumentException("maxIdle should not be negative.");
      }
      this.maxIdle = maxIdle;
    }

    if (minIdle != null) {
    
    
      if (minIdle < 0) {
    
    
        throw new IllegalArgumentException("minIdle should not be negative.");
      }
      this.minIdle = minIdle;
    }
  }
  //...省略getter方法...
}

Now, ResourcePoolConfig has only 4 configurable items, corresponding to the constructor, there are only 4 parameters, and the number of parameters is small. However, if the configurable items gradually increase and become 8, 10, or even more, then continue to follow the current design ideas, the parameter list of the constructor will become very long, and the code will be more readable and easy to use. It will get worse. When using the constructor, it is easy for us to misunderstand the order of the parameters and pass in the wrong parameter values, leading to very hidden bugs.


// 参数太多,导致可读性差、参数可能传递错误
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool", 16, null, 8, null, false , true, 10, 20falsetrue);

You should have already thought of the solution to this problem, and that is to use the set() function to assign member variables to replace the lengthy constructor. Let's look at the code directly, as shown below. Among them, the configuration item name is required, so we put it in the constructor to set it, and it must be filled out when creating a class object. Other configuration items maxTotal, maxIdle, minIdle are not required, so we set it through the set() function, allowing users to choose to fill in or not fill in.


public class ResourcePoolConfig {
    
    
  private static final int DEFAULT_MAX_TOTAL = 8;
  private static final int DEFAULT_MAX_IDLE = 8;
  private static final int DEFAULT_MIN_IDLE = 0;

  private String name;
  private int maxTotal = DEFAULT_MAX_TOTAL;
  private int maxIdle = DEFAULT_MAX_IDLE;
  private int minIdle = DEFAULT_MIN_IDLE;
  
  public ResourcePoolConfig(String name) {
    
    
    if (StringUtils.isBlank(name)) {
    
    
      throw new IllegalArgumentException("name should not be empty.");
    }
    this.name = name;
  }

  public void setMaxTotal(int maxTotal) {
    
    
    if (maxTotal <= 0) {
    
    
      throw new IllegalArgumentException("maxTotal should be positive.");
    }
    this.maxTotal = maxTotal;
  }

  public void setMaxIdle(int maxIdle) {
    
    
    if (maxIdle < 0) {
    
    
      throw new IllegalArgumentException("maxIdle should not be negative.");
    }
    this.maxIdle = maxIdle;
  }

  public void setMinIdle(int minIdle) {
    
    
    if (minIdle < 0) {
    
    
      throw new IllegalArgumentException("minIdle should not be negative.");
    }
    this.minIdle = minIdle;
  }
  //...省略getter方法...
}

Next, let's look at how to use the new ResourcePoolConfig class. I wrote a sample code as shown below. Without lengthy function calls and parameter lists, the code is much more readable and usable.


// ResourcePoolConfig使用举例
ResourcePoolConfig config = new ResourcePoolConfig("dbconnectionpool");
config.setMaxTotal(16);
config.setMaxIdle(8);

So far, we still haven't used the builder mode. By setting the required items through the constructor and optional configuration items through the set() method, we can achieve our design requirements. If we make the problem more difficult, for example, we still need to solve the following three problems, then the current design ideas cannot be satisfied.

● As we just mentioned, name is required, so we put it in the constructor and set it when creating an object forcibly. If there are many required configuration items, put these required configuration items in the constructor, then the constructor will have a long parameter list. If we set the required items through the set() method, then the logic of verifying whether these required items have been filled in is nowhere to be placed.

● In addition, it is assumed that there are certain dependencies between configuration items. For example, if the user sets one of maxTotal, maxIdle, and minIdle, the other two must be set explicitly; or there are certain constraints between configuration items Conditions, for example, maxIdle and minIdle must be less than or equal to maxTotal. If we continue to use the current design ideas, the verification logic of the dependencies or constraints between these configuration items will no longer be placed.

● If we want the ResourcePoolConfig class object to be an immutable object, that is, after the object is created, the internal property values ​​cannot be modified. To achieve this functionality, we cannot expose the set() method in the ResourcePoolConfig class.

In order to solve these problems, the builder mode comes in handy.


public class ResourcePoolConfig {
    
    
  private String name;
  private int maxTotal;
  private int maxIdle;
  private int minIdle;

  private ResourcePoolConfig(Builder builder) {
    
    
    this.name = builder.name;
    this.maxTotal = builder.maxTotal;
    this.maxIdle = builder.maxIdle;
    this.minIdle = builder.minIdle;
  }
  //...省略getter方法...

  //我们将Builder类设计成了ResourcePoolConfig的内部类。
  //我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
  public static class Builder {
    
    
    private static final int DEFAULT_MAX_TOTAL = 8;
    private static final int DEFAULT_MAX_IDLE = 8;
    private static final int DEFAULT_MIN_IDLE = 0;

    private String name;
    private int maxTotal = DEFAULT_MAX_TOTAL;
    private int maxIdle = DEFAULT_MAX_IDLE;
    private int minIdle = DEFAULT_MIN_IDLE;

    public ResourcePoolConfig build() {
    
    
      // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
      if (StringUtils.isBlank(name)) {
    
    
        throw new IllegalArgumentException("...");
      }
      if (maxIdle > maxTotal) {
    
    
        throw new IllegalArgumentException("...");
      }
      if (minIdle > maxTotal || minIdle > maxIdle) {
    
    
        throw new IllegalArgumentException("...");
      }

      return new ResourcePoolConfig(this);
    }

    public Builder setName(String name) {
    
    
      if (StringUtils.isBlank(name)) {
    
    
        throw new IllegalArgumentException("...");
      }
      this.name = name;
      return this;
    }

    public Builder setMaxTotal(int maxTotal) {
    
    
      if (maxTotal <= 0) {
    
    
        throw new IllegalArgumentException("...");
      }
      this.maxTotal = maxTotal;
      return this;
    }

    public Builder setMaxIdle(int maxIdle) {
    
    
      if (maxIdle < 0) {
    
    
        throw new IllegalArgumentException("...");
      }
      this.maxIdle = maxIdle;
      return this;
    }

    public Builder setMinIdle(int minIdle) {
    
    
      if (minIdle < 0) {
    
    
        throw new IllegalArgumentException("...");
      }
      this.minIdle = minIdle;
      return this;
    }
  }
}

// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
        .setName("dbconnectionpool")
        .setMaxTotal(16)
        .setMaxIdle(10)
        .setMinIdle(12)
        .build();

In fact, using the builder mode to create an object can also avoid the invalid state of the object. Let me explain with another example. For example, if we define a rectangular class, if we do not use the builder mode and adopt the method of creating and then set, it will cause the object to be in an invalid state after the first set. The specific code is as follows:


Rectangle r = new Rectange(); // r is invalid
r.setWidth(2); // r is invalid
r.setHeight(3); // r is valid

In order to avoid the existence of this invalid state, we need to use the constructor to initialize all the member variables at once. If there are too many parameters in the constructor, we need to consider using the builder mode, first set the variables of the builder, and then create the object once, so that the object is always in a valid state.

In fact, if we don't care about whether the object has a temporary invalid state, we don't care too much about whether the object is mutable. For example, if the object is only used to map the data read out from the database, then we directly expose the set() method to set the value of the member variable of the class. Moreover, using the builder pattern to build objects, the code is actually a bit repetitive. The member variables in the ResourcePoolConfig class must be redefined in the Builder class. What is the difference with the factory model?

What is the difference with the factory model?

From the above explanation, we can see that the builder mode is to let the builder class be responsible for the creation of the object. In the factory model mentioned in the previous lesson, the factory class is responsible for object creation. What is the difference between them?

In fact, the factory pattern is used to create different but related types of objects (a group of subclasses that inherit the same parent class or interface), and the given parameters determine which type of object to create. The builder mode is used to create a type of complex object, by setting different optional parameters, "customized" to create different objects.

There is a classic example on the Internet that explains the difference between the two.

Customers walk into a restaurant to order. We use the factory model to make different foods, such as pizza, burgers, and salads, according to different choices of users. For pizza, users have various ingredients to customize, such as cheese, tomatoes, and cheese. We use the builder mode to make pizza according to the different ingredients selected by the user.

In fact, we should not be too academic. We have to distinguish the factory model and the builder model so clearly. What we need to know is why each model is designed in such a way and what problems it can solve. Only by understanding these most essential things can we apply flexibly without copying them. We can even mix various models to create new models to solve problems in specific scenarios.

The builder mainly solves the problems of excessive parameters, parameter inspection, and immutability of control objects after creation

Guess you like

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