这篇文章来讲解比较常用的创建型设计模式-建造者模式。建造者模式主要用来建造复杂的对象。 本文UML类图链接为:https://www.processon.com/view/link/6080def6079129456d4beecf
本文代码链接为:https://github.com/shidawuhen/asap/blob/master/controller/design/9builder.go
1.定义
1.1建造者模式
建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
UML类图:
1.2分析
要理解定义,得先理解定义中的“表示”。“表示”可以简单的认为是类中成员变量值不同。例如要展示一个长方形,需要左上角和右下角,如果这些值不一样,“表示”也就不一样。
Builder的作用就是建造Product,建造Product太过复杂,所以Builder中有多个BuildPart()用于组装Product的部分元素,通过GetResult()获取建造好的Product对象。
又因为Builder中的BuildPart()方法太多,如果直接给客户端使用的话,调用方做组装成本会比较高,所以有Director,它会利用Builder中众多BuildPart()方法,将Product组合起来。然后客户端就能够通过Builder的GetResult()方便的获取到组装好的Product,而无需知道组装的细节。
2.使用场景
对于建造者的使用场景,《大话设计模式》和《设计模式之美》中讲的都不是特别理想。《大话设计模式》使用的是造胖小人、瘦小人的例子,属于为了讲解而强出的一个例子,好处是完全符合UML类图。《设计模式之美》里使用构建复杂对象的例子,好处是例子确实很常见,内核也是建造者模式的内核,但是实现上并不很符合UML类图。思考再三,还是选择实际一点的例子吧,毕竟学习设计模式就是为了具体使用的。
假设我们要创建一个资源池,需要设置资源名称(name)、最大总资源数量(maxTotal)、最大空闲资源数量(maxIdle)、最小空闲资源数量(minIdle)等。其中name必填,maxTotal、maxIdle、minIdle非必填,但是填了一个其它两个也需要填,而且数据值有限制,如不能等于0,数据间有限制,如maxIdle不能大于maxTotal。
碰到这种问题如何处理呢?
我们可以使用构造函数,所有的判断都在构造函数里做。但是一旦输入参数很多,会导致调用的时候容易写乱,而且构造函数里判断太多,后面需求有变化,构造函数也需要更改,不满足开放封闭原则。
如果使用set,因为数据间有限制,很容易漏掉部分配置。而且有时资源对象为不可变对象,就不能暴露set方法。
这个时候,建造者模式就能发挥作用了。建造者将输入数据整理好,将数据以对象的方式传递给资源类的构造函数,资源类拿到数据直接获取数值即可,是不是就达到了分离的效果。今后有规则上的变动,只需要修改Builder即可。
3.代码实现
package main
import (
"errors"
"fmt"
)
/**
* @Description: Product内的参数
*/
type ResourceParams struct {
name string
maxTotal int64
maxIdle int64
minIdle int64
}
/**
* @Description: Product接口
*/
type ResourceProduct interface {
show()
}
/**
* @Description: 实际Product,有show函数
*/
type RedisResourceProduct struct {
resourceParams ResourceParams
}
/**
* @Description: show成员函数,用于显示product的参数内容
* @receiver p
*/
func (p *RedisResourceProduct) show() {
fmt.Printf("Product的数据为 %+v ", p.resourceParams)
}
/**
* @Description: 资源类创建接口
*/
type ResourceBuilder interface {
setName(name string) ResourceBuilder
setMaxTotal(maxTotal int64) ResourceBuilder
setMaxIdle(maxIdle int64) ResourceBuilder
setMinIdle(minIdle int64) ResourceBuilder
getError() error
build() (p ResourceProduct)
}
/**
* @Description: 实际建造者
*/
type RedisResourceBuilder struct {
resourceParams ResourceParams
err error
}
/**
* @Description: 获取错误信息
* @receiver r
* @return error
*/
func (r *RedisResourceBuilder) getError() error {
return r.err
}
/**
* @Description: 设置名称
* @receiver r
* @param name
* @return ResourceBuilder
*/
func (r *RedisResourceBuilder) setName(name string) ResourceBuilder {
if name == "" {
r.err = errors.New("name为空")
return r
}
r.resourceParams.name = name
fmt.Println("RedisResourceBuilder setName ", name)
return r
}
/**
* @Description: 设置maxTotal值,值不能小于0
* @receiver r
* @param maxTotal
* @return ResourceBuilder
*/
func (r *RedisResourceBuilder) setMaxTotal(maxTotal int64) ResourceBuilder {
if maxTotal <= 0 {
r.err = errors.New("maxTotal小于0")
return r
}
r.resourceParams.maxTotal = maxTotal
fmt.Println("RedisResourceBuilder setMaxTotal ", maxTotal)
return r
}
/**
* @Description: 设置maxIdle值,值不能小于0
* @receiver r
* @param maxIdle
* @return ResourceBuilder
*/
func (r *RedisResourceBuilder) setMaxIdle(maxIdle int64) ResourceBuilder {
if maxIdle <= 0 {
r.err = errors.New("maxIdle小于0")
return r
}
r.resourceParams.maxIdle = maxIdle
fmt.Println("RedisResourceBuilder setMaxIdle ", maxIdle)
return r
}
/**
* @Description: 设置minIdle值,值不能小于0
* @receiver r
* @param minIdle
* @return ResourceBuilder
*/
func (r *RedisResourceBuilder) setMinIdle(minIdle int64) ResourceBuilder {
if minIdle <= 0 {
r.err = errors.New("minIdle小于0")
return r
}
r.resourceParams.minIdle = minIdle
fmt.Println("RedisResourceBuilder setMinIdle ", minIdle)
return r
}
/**
* @Description: 构建product
1. 做参数校验
2. 根据参数生成product
* @receiver r
* @return p
*/
func (r *RedisResourceBuilder) build() (p ResourceProduct) {
// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
if r.resourceParams.name == "" {
r.err = errors.New("name为空")
return
}
if !((r.resourceParams.maxIdle == 0 && r.resourceParams.minIdle == 0 && r.resourceParams.maxTotal == 0) ||
(r.resourceParams.maxIdle != 0 && r.resourceParams.minIdle != 0 && r.resourceParams.maxTotal != 0)) {
r.err = errors.New("数据需要保持一致")
return
}
if r.resourceParams.maxIdle > r.resourceParams.maxTotal {
r.err = errors.New("maxIdle > maxTotal")
return
}
if r.resourceParams.minIdle > r.resourceParams.maxTotal || r.resourceParams.minIdle > r.resourceParams.maxIdle {
r.err = errors.New("minIdle > maxTotal|maxIdle")
return
}
fmt.Println("RedisResourceBuilder build")
product := &RedisResourceProduct{
resourceParams: r.resourceParams,
}
return product
}
/**
* @Description: 指挥者
*/
type Director struct {
}
/**
* @Description: 指挥者控制建造过程
* @receiver d
* @param builder
* @return *ResourceProduct
*/
func (d *Director) construct(builder ResourceBuilder) ResourceProduct {
resourceProduct := builder.setName("redis").
setMinIdle(10).
setMaxIdle(10).
setMaxTotal(20).
build()
err := builder.getError()
if err != nil {
fmt.Println("构建失败,原因为" + err.Error())
return nil
}
return resourceProduct
}
func main() {
builder := &RedisResourceBuilder{}
director := &Director{}
product := director.construct(builder)
if product == nil {
return
}
product.show()
}
输出:
➜ myproject go run main.go
RedisResourceBuilder setName redis
RedisResourceBuilder setMinIdle 10
RedisResourceBuilder setMaxIdle 10
RedisResourceBuilder setMaxTotal 20
RedisResourceBuilder build
Product的数据为 {name:redis maxTotal:20 maxIdle:10 minIdle:10}
这段代码通过RedisResourceBuilder将Product的参数做了检查,使Product只需要关注自身的核心逻辑。Director负责组装,使调用方无需知道建造的细节。
如果需要更改为MySQL的资源,只需要创建MySQLResourceBuilder,然后实现接口函数即可。不过这种情况很少见。做的项目碰到的例子中,用在参数检验上相对多一些。
总结
如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合 set() 方法来解决。但是,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。
- 把类的必填属性放到构造函数中,强制创建对象的时候就设置
- 类的属性之间有一定的依赖关系或者约束条件
- 希望创建不可变对象
最后
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:https://shidawuhen.github.io/
往期文章回顾:
招聘
设计模式
- Go设计模式(8)-抽象工厂
- Go设计模式(7)-工厂模式
- Go设计模式(6)-单例模式
- Go设计模式(5)-类图符号表示法
- Go设计模式(4)-代码编写优化
- Go设计模式(4)-代码编写
- Go设计模式(3)-设计原则
- Go设计模式(2)-面向对象分析与设计
- Go设计模式(1)-语法
语言
架构
存储
网络
工具
读书笔记
思考