建造者模式

定义

Builder 模式,中文翻译为建造者模式或者构建者模式,也有人叫它生成器模式。

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。

看了定义还是晕晕的,这里来个栗子

这里按照设计模式之美中的资源池的例子来进行讨论

假设有这样一道设计面试题:我们需要定义一个资源池配置类ResourcePoolConfig。这里的资源池,你可以简单理解为线程池、连接池、对象池等。在这个资源池配置类中,有以下几个成员变量,也就是可配置项。现在,请你编写代码实现这个ResourcePoolConfig类。

成员变量 解释 是否必填 默认值
name 资源名称 没有
maxTotal 最大资源数量 10
maxIdle 最大空闲资源数量 10
minIdle 最小空闲资源数量 1

很简单,来看下代码实现

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
}

我们接着讨论,如果需要传入的参数过多,我们可以使用 set() 函数来给成员变量赋值,以替代冗长的构造函数。

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
}

到这里,我们还是没有用上建造者模式,我们来接着分析上面的栗子

1、上面的 name 字段是必填的,如果必填字段很多,那么我们的函数中又会出现参数很长的情况。当然必填项是不能放在set中设置的,因为如果对应的set没加,我们不能判断该参数必填的逻辑。

2、比如依赖关系,比如,如果用户设置了maxTotal、maxIdle、minIdle其中一个,就必须显式地设置另外两个;或者配置项之间有一定的约束条件,比如,maxIdle和minIdle要小于等于maxTotal。所以我们就需要一开始就知道所有的参数,才能进行对应校验。

3、如果我们希望ResourcePoolConfig类对象是不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值。要实现这个功能,我们就不能在ResourcePoolConfig类中暴露set()方法。

这时候建造者模式就登场了

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")
} // 设置默认值
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
}

建造者模式,避免了无效状态的存在,因为是设置构建者的变量,构建的变量符合条件之后,一次性的创建对象,这样创建的对象就一直处于有效状态了。

不过 go 中函数传值可以这样使用,一般公共库的时候都会选择这中方式,方便后期的扩展

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
}
}

相比于建造者模式,这种方式更其轻便,但是建造者模式也有有点,对于复杂参数的检验支持的更好

适用范围

1、类的必填属性放到构造函数中,强制创建对象的时候就设置。然后参数比较多,并且有必填校验

2、类的属性之间有一定的依赖关系或者约束条件

3、希望创建不可变对象

总结下就是

1、需要生成的对象具有复杂的内部结构。

2、需要生成的对象内部属性本身相互依赖。

与工厂模式的区别

工厂模式:工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象

建造者模式:建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。

来个栗子:

顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。

优点

1、建造者独立,易扩展。

2、便于控制细节风险。

缺点

1、产品必须有共同点,范围有限制。

2、如内部变化复杂,会有很多的建造类。

参考

【文中涉及到的代码】https://github.com/boilingfrog/design-pattern-learning/tree/master/建造者模式

【大话设计模式】https://book.douban.com/subject/2334288/

【极客时间】https://time.geekbang.org/column/intro/100039001

【建造者模式】https://www.runoob.com/design-pattern/builder-pattern.html

【Go设计模式03-建造者模式】https://lailin.xyz/post/builder.html

【建造者模式】https://boilingfrog.github.io/2021/11/06/使用go实现建造者模式/

设计模式学习-使用go实现建造者模式的更多相关文章

  1. C#设计模式学习笔记:(4)建造者模式

    本笔记摘抄自:https://www.cnblogs.com/PatrickLiu/p/7614630.html,记录一下学习过程以备后续查用. 一.引言 在现实生活中,我们经常会遇到一些构成比较复杂 ...

  2. JavaScript设计模式学习——builder pattern(建造者模式)

    个人理解的应用场景 举个例子,比如想要创建各种类型的车的实例,车的类型有很多种,但创建每种类型车的接口定义可能是一样的,就用到了此模式 相关概念的通俗解释 上述例子中接口的定义叫builder 接口到 ...

  3. 设计模式之第11章-建造者模式(Java实现)

    设计模式之第11章-建造者模式(Java实现) “那个餐厅我也是醉了...”“怎么了?”“上菜顺序啊,竟然先上甜品,然后是冷饮,再然后才是菜什么的,无语死了.”“这个顺序也有人这么点的啊.不过很少就是 ...

  4. C#设计模式学习笔记:(5)原型模式

    本笔记摘抄自:https://www.cnblogs.com/PatrickLiu/p/7640873.html,记录一下学习过程以备后续查用.  一.引言 很多人说原型设计模式会节省机器内存,他们说 ...

  5. .NET设计模式(4):建造者模式(Builder Pattern)(转)

    概述 在软件系统中,有时候面临着“一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成:由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法确相对稳定. ...

  6. .NET设计模式(4):建造者模式(Builder Pattern)

    ):建造者模式(Builder Pattern)    .建造者模式的使用使得产品的内部表象可以独立的变化.使用建造者模式可以使客户端不必知道产品内部组成的细节. 2.每一个Builder都相对独立, ...

  7. java设计模式(二)单例模式 建造者模式

    (三)单例模式 单例模式应该是最常见的设计模式,作用是保证在JVM中,该对象仅仅有一个实例存在. 长处:1.降低某些创建比較频繁的或者比較大型的对象的系统开销. 2.省去了new操作符,减少系统内存使 ...

  8. Java设计模式(四)Builder建造者模式

    一.场景描述 建造者模式同工厂模式.抽象工厂模式一样,用于创建继承类对象. 工厂模式:http://www.cnblogs.com/mahongbiao/p/8618970.html 抽象工厂模式:h ...

  9. JAVA设计模式之简单粗暴学建造者模式

    文章由浅入深,先用简单例子说明建造者,然后分析模式的优缺点,最后结合优秀开源框架Mybatis,说明该模式的用处. 1.先定义一个机器人模型 package com.jstao.model; publ ...

随机推荐

  1. DISCUZ论坛添加页头及页尾背景图片的几种方法

    先给大家分享页头添加背景图片的两种方法:1. 第一种效果,是给discuz的整体框架添加背景图片,见图示: 添加方法如下:找到你现在使用模板common文件下的header.html文件,在<h ...

  2. OC源码剖析对象的本质

    1. 类的底层实现 先写一个 Person 类: @interface Person : NSObject @property (nonatomic, copy) NSString *p_name; ...

  3. CF438E-The Child and Binary Tree【生成函数】

    正题 题目链接:https://www.luogu.com.cn/problem/CF438E 题目大意 每个节点有\(n\)个权值可以选择,对于\(1\sim m\)中的每个数字\(k\),求权值和 ...

  4. react-native移动端设置android闪屏页

    前言 因为app启动时会白屏一段时间,导致让人用起来非常的不舒服,后来了解一下知道这叫做闪屏 于是着手解决这个白屏的问题,换个颜色?不行,不如用一张好看的图片来替换,这样才让人看起来更加舒服. 那么该 ...

  5. Semi-supervised semantic segmentation needs strong, varied perturbations

    论文阅读: Semi-supervised semantic segmentation needs strong, varied perturbations 作者声明 版权声明:本文为博主原创文章,遵 ...

  6. 记录一次基于VuePress + Github 搭建个人博客

    最终效果图 网站:https://chandler712.github.io/ 一.前言 VuePress 是尤雨溪推出的支持 Vue 及其子项目的文档需求而写的一个项目,UI简洁大方,官方文档详细容 ...

  7. 使用 grpcurl 通过命令行访问 gRPC 服务

    原文链接: 使用 grpcurl 通过命令行访问 gRPC 服务 一般情况下测试 gRPC 服务,都是通过客户端来直接请求服务端.如果客户端还没准备好的话,也可以使用 BloomRPC 这样的 GUI ...

  8. 题解 [HNOI2012]集合选数

    题目传送门 题目大意 直接看题面吧. 思路 感觉挺水的一道题啊?怎么评到紫色的啊?考试的时候LJS出了这个题的加强版我就只想出这个思路,然后就爆了... 不难发现,我们可以构造矩阵: x 2x 4x ...

  9. 【Python】 第三周:基本数据类型

    整数 python整数无限制 二进制:以0b或者0B开头,例如: 0b010,-0B101 八进制:以0o或者0O开头,例如:0o123,-0O456 浮点数 浮点数间运算存在不确定尾数,不是bug ...

  10. Redis使用过程中有哪些注意事项?看看BAT这类的公司是正确使用Redis的!!

    Redis使用过程中要注意的事项 Redis使用起来很简单,但是在实际应用过程中,一定会碰到一些比较麻烦的问题,常见的问题有 redis和数据库数据的一致性 缓存雪崩 缓存穿透 热点数据发现 下面逐一 ...