[builder mode] of design mode

Posted by knetcozd on Sun, 06 Mar 2022 04:49:10 +0100

The design principle is a summary of some experience guiding our code design, that is, "mental method"; Object oriented is our "weapon"; Design pattern is "move".

Based on mental skill, use weapon moves to deal with complex programming problems.

Why is McDonald's so popular?

Cousin: brother, I want to eat McDonald's

Me: why do you like McDonald's so much?

Cousin: because it's delicious, and the taste of each store is the same. Unlike snacks in a county, the taste of each place is different.

Me: do you know why?

Cousin: because McDonald's has a very perfect workflow, every store must abide by this set of norms

Isn't this the "builder mode" in our design mode?

Separate the construction of a complex object from its representation, so that the same construction process can create different representations.

We take soup making as an example. We know that the main soup ingredients and water are necessary for soup making, and the ingredients can be put or not. Some people like to eat the original juice and flavor without salt. Some meat is more oily, so you can also put no oil.

Let's take a look at the specific code implementation:

 1 public class Soup {
 2     private static final int MAX_INGREDIENTS = 10;
 3     private static final int MAX_OIL = 8;
 4     private static final int MAX_SALT = 5;
 5     
 6     private String mainIngredient;  // Main material must
 7     private String water;           // Water must
 8     private int ingredients;        // Optional ingredients
 9     private int oil;                // Oil optional
10     private int salt;               // Salt optional
11     
12     public Soup(String main, String water, Integer ingredients, Integer oil, Integer salt) {
13         if (StringUtils.isBlank(main)) {
14             throw new IllegalArgumentException("main should not be empty");
15         }
16         this.mainIngredient = main;
17     
18         if (StringUtils.isBlank(water)) {
19             throw new IllegalArgumentException("water should not be empty");
20         }
21         this.water = water;
22     
23         if (ingredients != null) {
24             if (ingredients < 0) {
25                 throw new IllegalArgumentException("ingredients should not be positive");
26             }
27             this.ingredients = ingredients;
28         }
29         
30         if (oil != null) {
31             if (oil < 0) {
32                 throw new IllegalArgumentException("oil should not be positive");
33             }
34             this.oil = oil;
35         }
36         
37         if (salt != null) {
38             if (salt < 0) {
39                 throw new IllegalArgumentException("salt should not be positive");
40             }
41             this.salt = salt;
42         }
43     }
44     
45     // ellipsis get method
46 }
47 48 // I'd like fish head and tofu soup today
49 Soup fishHeadTofuSoup = new Soup("Fish head", "Mountain spring water", 10, 6, 3);

As you can see, this constructor has five parameters. The parameter list is too long, resulting in poor readability and ease of use of the code.

Moreover, with such a long parameter list and the same parameter types, when using the constructor, it is easy to get the order of each parameter wrong and pass in the wrong parameter value, resulting in a very hidden bug. In the above example, the ingredients and salt are of type int. if I accidentally swap the positions of these two parameters into 3 grams of ingredients and 10 grams of salt, can I still eat?

Some students may say that some properties of your class are optional when constructing objects. These properties can be set through the set() function.

Yes, let's take a look at the implementation effect of this method:

 1 public class Soup {
 2     private static final int MAX_INGREDIENTS = 10;
 3     private static final int MAX_OIL = 8;
 4     private static final int MAX_SALT = 5;
 5     
 6     private String mainIngredient;  // Main material must
 7     private String water;           // Water must
 8     private int ingredients;        // Optional ingredients
 9     private int oil;                // Oil optional
10     private int salt;               // Salt optional
11     
12     public Soup(String main, String water) {
13         if (StringUtils.isBlank(main)) {
14             throw new IllegalArgumentException("main should not be empty");
15         }
16         this.mainIngredient = main;
17     
18         if (StringUtils.isBlank(water)) {
19             throw new IllegalArgumentException("water should not be empty");
20         }
21         this.water = water;
22     }
23     
24     public void setIngredients(int ingredients) {
25         if (ingredients != null) {
26             if (ingredients < 0) {
27                 throw new IllegalArgumentException("ingredients should not be positive");
28             }
29             this.ingredients = ingredients;
30         }
31     }
32      
33     public void setOil(int oil) {
34         if (oil != null) {
35             if (oil < 0) {
36                 throw new IllegalArgumentException("oil should not be positive");
37             }
38             this.oil = oil;
39         }
40     }
41     
42     public void setSalt(int salt) {
43         if (salt != null) {
44             if (salt < 0) {
45                 throw new IllegalArgumentException("salt should not be positive");
46             }
47             this.salt = salt;
48         }
49     }
50     
51     // ellipsis get method
52 }
53 54 // I'd like mushroom and black chicken soup today
55 Soup blackChickenSoup = new Soup("Black chicken", "water");
56 blackChickenSoup.setIngredients(8);
57 blackChickenSoup.setOil(5);
58 blackChickenSoup.setSalt(3);

In this way, it is true that the parameter list of the constructor has become shorter and can make delicious soup. However, the following problems still exist:

1. In the above example, there are only two required attributes, but what if there are many required attributes? Put these required attributes into the constructor and set them. Isn't that another long parameter list?

2. If we also set the required attributes through the set() method, there will be no place to put the logic to check whether these required attributes have been filled in.

3. If there is a certain dependency between attributes, for example, if the soup maker puts ingredients ingredients, salt must be added; Or there are certain constraints between attributes. For example, the grams of ingredients are greater than the grams of salt. If we continue to use the current design ideas, there will be no place for the verification logic of the dependencies or constraints between these attributes.

4. If we want the Soup class object to be immutable, that is, after the object is created, we can't modify the internal attribute value. Then, we can't expose the set() method in the class.

5. If these properties are not initialized together, it will lead to the invalid state of the object. What is an invalid state? Let's look at the following code:

1 Rectangle r = new Rectangle();  // here r Is an invalid object because a rectangle must have an aspect value.
2 r.setWidth(2);  // here r Or invalid status
3 r.setHeight(4); // At this time r Only when the length width value is set is the effective state

At this time, the builder model comes in handy~

Builder pattern

1. Create a static inner class Builder in the Soup class, and then copy all the parameters in the Soup class to the Builder class.

2. Create a private constructor in the Soup class with the parameter of Builder type.

3. Create a public constructor in Builder. The parameters are required in Soup, maingradient and water.

4. Create a set() method in Builder, assign values to those optional parameters in the Soup, and the return value is an instance of Builder type.

5. Create a build() method in Builder, create an instance of Soup and return it.

  1 public class Soup {
  2     private String mainIngredient;
  3     private String water;
  4     private int ingredient;
  5     private int oil;
  6     private int salt;
  7     
  8     private Soup(Builder builder) {
  9         this.mainIngredient = builder.mainIgredient;
 10         this.water = builder.water;
 11         this.ingredients = builder.ingredients;
 12         this.oil = builder.oil;
 13         this.salt = builder.salt;
 14     }
 15     // ellipsis get method
 16     
 17     // take Builder Class design Soup Inner class of
 18     // You can also BUilder Class is designed as a separate non inner class SoupBuilder
 19     public static class Builder {
 20         private static final int MAX_INGREDIENTS = 10;
 21         private static final int MAX_OIL = 8;
 22         private static final int MAX_SALT = 5;
 23         
 24         private String mainIngredient;
 25         private String water;
 26         private int ingredient;
 27         private int oil;
 28         private int salt;
 29         
 30         public Soup build() {
 31             // Verification logic is put here, including required item verification, dependency verification, constraint verification, etc
 32             // Main material required
 33             if (StringUtils.isBlank(mainIngredient)) {
 34                 throw new IllegalArgumentException("...");
 35             }
 36             // Water required
 37             if (StringUtils.isBlank(water)) {
 38                 throw new IllegalArgumentException("...");
 39             }
 40             // Dependency: if you put ingredients, you must put salt
 41             if (ingredients > 0 && salt <= 0) {
 42                 throw new IllegalArgumentException("...");
 43             }
 44             // Constraints: the grams of ingredients cannot be less than or equal to the grams of salt
 45             if (ingredients <= salt) {
 46                 throw new IllegalArgumentException("...");
 47             }
 48             
 49             return new Soup(this);
 50         }
 51         
 52         public Builder setMainIngredients(String mainIngredient) {
 53             if (StringUtils.isBlank(mainIngredient)) {
 54                 throw new IllegalArgumentException("...");
 55             }
 56             this.mainIngredient = mainIngredient;
 57             return this;
 58         }
 59  
 60         public Builder setWater(String water) {
 61             if (StringUtils.isBlank(water)) {
 62                 throw new IllegalArgumentException("...");
 63             }
 64             this.water = water;
 65             return this;
 66         }
 67         
 68         public Builder setIngredients(int ingredients) {
 69             if (ingredients != null) {
 70                 if (ingredients < 0) {
 71                     throw new IllegalArgumentException("ingredients should not be positive");
 72                 }
 73                 this.ingredients = ingredients;
 74             }
 75             return this;
 76         }
 77      
 78         public Builder setOil(int oil) {
 79             if (oil != null) {
 80                 if (oil < 0) {
 81                     throw new IllegalArgumentException("oil should not be positive");
 82                 }
 83                 this.oil = oil;
 84             }
 85             return this;
 86         }
 87  88         public Builder setSalt(int salt) {
 89             if (salt != null) {
 90                 if (salt < 0) {
 91                     throw new IllegalArgumentException("salt should not be positive");
 92                 }
 93                 this.salt = salt;
 94             }
 95             return this;
 96         }
 97     }
 98 }
 99 100 // Today I have white gourd ribs soup
101 Soup winterMelonRibSoup = new Soup.Builder()
102     .setMainIngredients("spareribs")
103     .setWater("Mountain spring water")
104     .setIngredients(8)   // White gourd 8g
105     .setOil(2)
106     .setSalt(3)
107     .builder();

You see, with this design, the above five problems are solved.

Have you found that a delicious soup is composed of several simple objects: main ingredients, water, ingredients, oil and salt, and then constructed step by step. The builder mode will separate change from invariance, that is, the components of the soup are invariable, but each part can be flexibly selected.

Like the white gourd spareribs soup cooked on it, if I forget to put the oil:

1 Soup winterMelonRibSoup = new Soup.Builder()
2     .setMainIngredients("spareribs")   // First step
3     .setWater("Mountain spring water")
4     .setIngredients(8)   // White gourd 8g
5     // .setOil(2)        // No oil drain
6     .setSalt(3)       
7     .builder();

This will not lead to invalid status. If there is no display setting, it will be automatically initialized to the default value. Then, the taste is different.

Some people may ask that the above definition says "separate the construction of a complex object from its representation, so that the same construction process can create different representations", but you have not separated the construction from the representation, and different construction processes create different representations.

Yes, the same construction process is equivalent to McDonald's standard production process. If you build objects according to this construction process, you won't forget to "drain the oil". As shown in the figure below:

 

In fact, the above is a simplified way of using Builder in Java, and the classical Builder mode is still a little different.

Classic Builder mode

It has four roles:

  • Product: the final object to be generated, such as the Soup instance.

  • Builder: the abstract base class of the builder (sometimes replaced by an interface). It defines the abstract steps of building a Product, and its body class needs to implement these steps. It will contain a method Product getProduct() to return the final Product.

  • ConcreteBuilder: the implementation class of Builder.

  • Director: the step of deciding how to build the final product. It will include a method void Construct(Builder builder) responsible for assembly. In this method, the builder can be set by calling the builder's method. After the setting is completed, the final product can be obtained through the builder's getProduct() method.

Next, let's look at the specific code implementation:

Step 1: our target Soup class:

 1 public class Soup {
 2     private static final int MAX_INGREDIENTS = 10;
 3     private static final int MAX_OIL = 8;
 4     private static final int MAX_SALT = 5;
 5     
 6     private String mainIngredient;  // Main material must
 7     private String water;           // Water must
 8     private int ingredients;        // Optional ingredients
 9     private int oil;                // Oil optional
10     private int salt;               // Salt optional
11     
12     public Soup(String main, String water) {
13         if (StringUtils.isBlank(main)) {
14             throw new IllegalArgumentException("main should not be empty");
15         }
16         this.mainIngredient = main;
17     
18         if (StringUtils.isBlank(water)) {
19             throw new IllegalArgumentException("water should not be empty");
20         }
21         this.water = water;
22     }
23     
24     public void setIngredients(int ingredients) {
25         if (ingredients != null) {
26             if (ingredients < 0) {
27                 throw new IllegalArgumentException("ingredients should not be positive");
28             }
29             this.ingredients = ingredients;
30         }
31     }
32      
33     public void setOil(int oil) {
34         if (oil != null) {
35             if (oil < 0) {
36                 throw new IllegalArgumentException("oil should not be positive");
37             }
38             this.oil = oil;
39         }
40     }
41     
42     public void setSalt(int salt) {
43         if (salt != null) {
44             if (salt < 0) {
45                 throw new IllegalArgumentException("salt should not be positive");
46             }
47             this.salt = salt;
48         }
49     }
50     
51     // ellipsis get method
52 }

Step 2: Abstract builder class

1 public abstract class SoupBuilder {
2     public abstract void setIngredients();
3     public abstract void setSalt();
4     public abstract void setOil();
5     
6     public abstract Soup getSoup();
7 }

Step 3: entity builder class. We can generate multiple entity builder classes according to the type of products to be built. Here we build two kinds of soup, fish head tofu soup and white gourd ribs soup. Therefore, we generated two entity builder classes.

Fish head tofu soup Builders:

 1 public class FishHeadTofuSoupBuilder extends SoupBuilder {
 2     private Soup soup;
 3     public (String mainIngredient, String water) {
 4         soup = new Soup(mainIngredient, water);
 5     }
 6     @Override
 7     public void setIngredients() {
 8         soup.setIngredients(10);
 9     }
10     @Override 
11     public void setSalt() {
12         soup.setSalt(3);
13     }
14     @Override 
15     public void setOil() {
16         soup.setOil(4);
17     }
18     
19     @Override 
20     public Soup getSoup() {
21         return soup;
22     }
23 }

Builders of white gourd ribs soup:

 1 public class WinterMelonRibSoupBuilder extends SoupBuilder {
 2     private Soup soup;
 3     public (String mainIngredient, String water) {
 4         soup = new Soup(mainIngredient, water);
 5     }
 6     @Override
 7     public void setIngredients() {
 8         soup.setIngredients(7);
 9     }
10     @Override 
11     public void setSalt() {
12         soup.setSalt(2);
13     }
14     @Override 
15     public void setOil() {
16         soup.setOil(3);
17     }
18     
19     @Override 
20     public Soup getSoup() {
21         return soup;
22     }
23 }

Step 4: Director

1 public class SoupDirector {
2     public void makeSoup(SoupBuilder builder) {
3         // A set of standardized processes
4         builder.setIngredients();
5         builder.setSalt();
6         builder.setOil();
7     }
8 }

The director class SoupDirector plays a very important role in the Builder mode. It is used to guide the specific Builder how to build the product and control the call order. This is what the definition calls "using the same build process".

So let's take a look at how clients build objects?

 1 public static void main(String[] args) {
 2     SoupDirector director = new SoupDirector();
 3     SoupBuilder builder = new FishHeadTofuSoupBuilder("Fish head", "water");
 4     // Follow that standard process to make soup
 5     director.makeSoup(builder);
 6     // Fish head tofu soup out of the pot
 7     Soup fishHeadTofuSoup = builder.getSoup();
 8     
 9     // Now I'd like to have white gourd ribs soup
10     SoupBuilder winterMelonRibSoupBuilder = new WinterMelonRibSoupBuilder("spareribs", "Mountain spring water");
11     // Follow that standard process to make soup
12     director.makeSoup(winterMelonRibSoupBuilder);
13     // Stewed pork ribs with white gourd
14     Soup winterMelonRibSoup = winterMelonRibSoupBuilder.getSoup();
15 }

Advantages of builder model

  • Using the builder mode can make the client do not have to know the details of the internal composition of the product.

  • The specific builder classes are independent of each other, which is conducive to the expansion of the system.

  • The specific builders are independent of each other, so the construction process can be gradually refined without any impact on other modules.

Disadvantages of the builder model

  • The products created by the builder model generally have more in common, and their components are similar; If there are great differences between products, it is not suitable to use the builder mode. Therefore, its scope of use is limited.

  • If the internal changes of the product are complex, it may lead to the need to define many specific builder classes to realize this change, resulting in a huge system and reduced maintainability.

Difference between builder mode and factory mode

Through the previous study, let's take a look at the builder model and Factory method model What's the difference?

  • Different intentions

In the factory method mode, we focus on a product as a whole, such as the tank as a whole, and we don't need to care about how each part of the tank is created; However, in the builder mode, the generation of a specific product depends on the generation and assembly sequence of various components. It focuses on "assembling the product object step by step from the parts". In short, factory mode is a thick line application of object creation, while builder mode outlines a complex object through thin lines, focusing on the creation process of product components.

  • The complexity of products is different

The products created by the factory method pattern are generally single products. For example, tank has a method attack(), while the builder pattern creates a composite product, which is composed of various parts. Of course, the product objects are different for different parts. This does not mean that the objects created by the factory method pattern are simple, but that their granularity is different. Generally speaking, the object granularity of factory method pattern is relatively coarse, and the product object granularity of builder pattern is relatively fine.

Application scenario of builder mode

When the product to be created has a complex creation process, the common creation process can be extracted and then handed over to the specific implementation class to customize the creation process, so that the same creation behavior can produce different products, separating creation and representation, and greatly increasing the flexibility of creating products.

The builder mode is mainly applicable to the following scenarios:

  • The same method and different execution order produce different results.

  • Multiple assemblies or parts can be assembled into one object, but the results are different.

  • The product class is very complex, or different call sequences in the product class have different effects.

  • Initializing an object is complex with many parameters, and many parameters have default values.

summary

Builder mode is used to create complex objects. Different objects can be created "customized" by setting different optional parameters.

reference resources

Geek time column "the beauty of design patterns"

https://www.cnblogs.com/ChinaHook/p/7471470.html

https://zhuanlan.zhihu.com/p/58093669

Topics: Java Design Pattern