Builder pattern
definition
Builder mode is translated into builder mode or builder mode in Chinese. It is also called builder mode.
Builder Pattern uses multiple simple objects to build a complex object step by step. This type of design pattern is a creation pattern, which provides the best way to create objects.
A Builder class will construct the final object step by step. The Builder class is independent of other objects.
I'm still dizzy after reading the definition. Here's a chestnut
Here according to Beauty of design pattern Examples of resource pools in are discussed
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, that is, configurable items. Now, please write code to implement this ResourcePoolConfig class.
Member variable | explain | Required | Default value |
---|---|---|---|
name | Resource name | yes | No, |
maxTotal | Maximum number of resources | no | 10 |
maxIdle | Maximum number of free resources | no | 10 |
minIdle | Minimum number of free resources | no | 1 |
Very simple, let's look at the code implementation
const ( defaultMaxTotal = 10 defaultMaxIdle = 10 defaultMinIdle = 1 ) // ResourcePoolConfig ... type ResourcePoolConfig struct { name string maxTotal int maxIdle int minIdle int } func NewResourcePoolConfig(name string, maxTotal, maxIdle, minIdle *int) (*ResourcePoolConfig, error) { rc := &ResourcePoolConfig{ maxTotal: defaultMaxTotal, maxIdle: defaultMaxIdle, minIdle: defaultMinIdle, } if name == "" { return nil, errors.New("name is empty") } rc.name = name if maxTotal != nil { if *maxTotal <= 0 { return nil, errors.New("maxTotal should be positive") } rc.maxTotal = *maxTotal } if maxIdle != nil { if *maxIdle <= 0 { return nil, errors.New("maxIdle should not be negative") } rc.maxIdle = *maxIdle } if minIdle != nil { if *minIdle <= 0 { return nil, errors.New("minIdle should not be negative") } rc.minIdle = *minIdle } return rc, nil }
We then discuss that if too many parameters need to be passed in, we can use the set() function to assign values to member variables instead of lengthy constructors.
const ( defaultMaxTotal = 10 defaultMaxIdle = 10 defaultMinIdle = 1 ) // ResourcePoolConfig ... type ResourcePoolConfig struct { name string maxTotal int maxIdle int minIdle int } func NewResourcePoolConfigSet(name string) (*ResourcePoolConfig, error) { if name == "" { return nil, errors.New("name is empty") } return &ResourcePoolConfig{ maxTotal: defaultMaxTotal, maxIdle: defaultMaxIdle, minIdle: defaultMinIdle, name: name, }, nil } // SetMinIdle ... func (rc *ResourcePoolConfig) SetMinIdle(minIdle int) error { if minIdle < 0 { return fmt.Errorf("min idle cannot < 0, input: %d", minIdle) } rc.minIdle = minIdle return nil } // SetMaxIdle ... func (rc *ResourcePoolConfig) SetMaxIdle(maxIdle int) error { if maxIdle < 0 { return fmt.Errorf("max idle cannot < 0, input: %d", maxIdle) } rc.maxIdle = maxIdle return nil } // SetMaxTotal ... func (rc *ResourcePoolConfig) SetMaxTotal(maxTotal int) error { if maxTotal <= 0 { return fmt.Errorf("max total cannot <= 0, input: %d", maxTotal) } rc.maxTotal = maxTotal return nil }
So far, we still haven't used the builder model. Let's continue to analyze the chestnuts above
1. The name field above is required. If there are many required fields, there will be long parameters in our function. Of course, mandatory items cannot be set in set, because if the corresponding set is not added, we cannot judge the logic of mandatory parameters.
2. For example, if the user sets one of maxTotal, maxIdle and minIdle, the other two must be explicitly set; Or there are certain constraints between configuration items. For example, maxIdle and minIdle should be less than or equal to maxTotal. Therefore, we need to know all the parameters from the beginning to perform corresponding verification.
3. If we want the ResourcePoolConfig class object to be immutable, that is, after the object is created, the internal attribute value cannot be modified. To achieve this, we cannot expose the set() method in the ResourcePoolConfig class.
At this time, the builder mode comes into play
const ( defaultMaxTotal = 10 defaultMaxIdle = 10 defaultMinIdle = 1 ) // ResourcePoolConfig ... type ResourcePoolConfig struct { name string maxTotal int maxIdle int minIdle int } // ResourcePoolConfigBuilder ... type ResourcePoolConfigBuilder struct { name string maxTotal int maxIdle int minIdle int } // SetName ... func (rb *ResourcePoolConfigBuilder) SetName(name string) error { if name == "" { return fmt.Errorf("name can not be empty") } rb.name = name return nil } // SetMinIdle ... func (rb *ResourcePoolConfigBuilder) SetMinIdle(minIdle int) error { if minIdle < 0 { return fmt.Errorf("max total cannot < 0, input: %d", minIdle) } rb.minIdle = minIdle return nil } // SetMaxIdle ... func (rb *ResourcePoolConfigBuilder) SetMaxIdle(maxIdle int) error { if maxIdle < 0 { return fmt.Errorf("max total cannot < 0, input: %d", maxIdle) } rb.maxIdle = maxIdle return nil } // SetMaxTotal ... func (rb *ResourcePoolConfigBuilder) SetMaxTotal(maxTotal int) error { if maxTotal <= 0 { return fmt.Errorf("max total cannot <= 0, input: %d", maxTotal) } rb.maxTotal = maxTotal return nil } // Build ... func (rb *ResourcePoolConfigBuilder) Build() (*ResourcePoolConfig, error) { if rb.name == "" { return nil, errors.New("name can not be empty") } // Set defaults if rb.minIdle == 0 { rb.minIdle = defaultMinIdle } if rb.maxIdle == 0 { rb.maxIdle = defaultMaxIdle } if rb.maxTotal == 0 { rb.maxTotal = defaultMaxTotal } if rb.maxTotal < rb.maxIdle { return nil, fmt.Errorf("max total(%d) cannot < max idle(%d)", rb.maxTotal, rb.maxIdle) } if rb.minIdle > rb.maxIdle { return nil, fmt.Errorf("max idle(%d) cannot < min idle(%d)", rb.maxIdle, rb.minIdle) } return &ResourcePoolConfig{ name: rb.name, maxTotal: rb.maxTotal, maxIdle: rb.maxIdle, minIdle: rb.minIdle, }, nil }
The builder mode avoids the existence of invalid state, because it is to set the builder's variables. After the constructed variables meet the conditions, the object will be created at one time, so that the created object will always be in an effective state.
However, the function value transfer in go can be used in this way. Generally, this method will be selected in the public library to facilitate later expansion
const ( defaultMaxTotal = 10 defaultMaxIdle = 10 defaultMinIdle = 1 ) // ResourcePoolConfig ... type ResourcePoolConfig struct { name string maxTotal int maxIdle int minIdle int } type Param func(*ResourcePoolConfig) func NewResourcePoolConfigOption(name string, param ...Param) (*ResourcePoolConfig, error) { if name == "" { return nil, errors.New("name is empty") } ps := &ResourcePoolConfig{ maxIdle: defaultMinIdle, minIdle: defaultMinIdle, maxTotal: defaultMaxTotal, name: name, } for _, p := range param { p(ps) } if ps.maxTotal < 0 || ps.maxIdle < 0 || ps.minIdle < 0 { return nil, fmt.Errorf("args err, option: %v", ps) } if ps.maxTotal < ps.maxIdle || ps.minIdle > ps.maxIdle { return nil, fmt.Errorf("args err, option: %v", ps) } return ps, nil } func MaxTotal(maxTotal int) Param { return func(o *ResourcePoolConfig) { o.maxTotal = maxTotal } } func MaxIdle(maxIdle int) Param { return func(o *ResourcePoolConfig) { o.maxIdle = maxIdle } } func MinIdle(minIdle int) Param { return func(o *ResourcePoolConfig) { o.minIdle = minIdle } }
Compared with the builder mode, this method is more portable, but the builder mode is also a little, and it supports the inspection of complex parameters better
Scope of application
1. The required properties of the class are placed in the constructor and set when the object is forcibly created. Then, there are many parameters, and there are required verifications
2. There are certain dependencies or constraints between the attributes of a class
3. You want to create immutable objects
To sum up
1. The object to be generated has a complex internal structure.
2. The internal properties of the object to be generated depend on each other.
Difference from factory mode
Factory mode: factory mode is used to create objects of different but related types (inheriting a group of subclasses of the same parent class or interface). The given parameters determine which type of object to create
Builder mode: Builder mode is used to create a type of complex object. Different objects are created "customized" by setting different optional parameters.
Have a chestnut:
When customers order in a restaurant, we use the factory model to make different foods according to users' different choices, such as pizza, hamburger and salad. For pizza, users can customize various ingredients, such as cheese, tomato and cheese. We make pizza according to different ingredients selected by users through the builder mode.
advantage
1. The builder is independent and easy to expand.
2. Easy to control detail risk.
shortcoming
1. Products must have common ground and limited scope.
2. If the internal changes are complex, there will be many construction classes.
reference resources
[codes involved in the text] https://github.com/boilingfrog/design-pattern-learning/tree/master/ Builder pattern
[big talk design mode] https://book.douban.com/subject/2334288/
[geek time] https://time.geekbang.org/column/intro/100039001
[builder mode] https://www.runoob.com/design-pattern/builder-pattern.html
[Go design mode 03 builder mode] https://lailin.xyz/post/builder.html
[builder mode] https://boilingfrog.github.io/2021/11/06/ Using go to implement builder mode/