生成器模式-代码的艺术系列
Posted 晓旭z
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了生成器模式-代码的艺术系列相关的知识,希望对你有一定的参考价值。
前言
看代码"文档",学设计模式
网上讲解设计模式都是基于现实生活场景
举例,但作为 coder ,还需要有将现实生活场景
到代码实现场景
的转化思维,所以我认为,了解设计模式简单,实践到对应代码场景中有难度。
so 我们的代码的艺术系列
会以还原 coding 现场
的方式,讲诉设计模式
!
生成器模式
来看概念:
生成器模式是一种创建型
设计模式,也叫建造者模式
。它把对象的创建步骤
抽象成生成
器,并且可以通过指导类(director
)对所有生成步骤的先后顺序
进行控制
。客户端使用指导类并传入相应的生成器,通过指导类的接口便可以得到相应的对象。
The intent of the Builder design pattern is to separate the construction of a complex object from its representation. By doing so the same construction process can create different representations.
将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示
概括的说:有些对象的创建流程是一样的,但是因为自身特性的不同
,所以在创建他们的时候需要将创建过程
和个性化的属性
分离出来。
基本看不懂啥意思,赞
! 继续看结构~
来看结构:
- 生成器 (Builder) 接口声明在所有类型生成器中通用的产品构造步骤。
- 具体生成器 (Concrete Builders) 提供构造过程的不同实现。 具体生成器也可以构造不遵循通用接口的产品。
- 产品 (Products) 是最终生成的对象。 由不同生成器构造的产品无需属于同一类层次结构或接口。
- 主管 (Director) 类定义调用构造步骤的顺序, 这样你就可以创建和复用特定的产品配置。
- 客户端 (Client) 必须将某个生成器对象与主管类关联。 一般情况下, 你只需通过主管类构造函数的参数进行一次性关联即可。 此后主管类就能使用生成器对象完成后续所有的构造任务。
实战学习
概念性描述你懂了么?不懂就对了,程序员先理解概念不如直接上代码来的刺激。
这里贯彻下我们前言中的宗旨:
- 不要生活场景的举例,要产品需求的案例。
- 不要纯图文的描述,要可阅读的代码。
上实战~
产品需求
没有产品思维的程序员不是好销售,此需求纯属虚构
pm 要在自家的电商网站电脑产品垂类下增加报价功能。
PRD 描述当用户进入 mac 品牌的详情页,他可以选择 I7 CPU, 500G 内存,1T 磁盘的配置,查看报价。
不同品牌的部件价格不相同,并且不同品牌在同一时刻有不同的优惠折扣。
需求目标: 实时计算出用户选择的电脑配置折后价钱。
需求收益: 提高下单率50%
技术文档
技术脑爆时刻到了!
乍一听感觉很简单,没什么复杂逻辑。
其实真的很简单。但是问题是不同品牌的部件配置价格不同,而且不同品牌的折扣也是不同的。用户选择了 A,B,C-Z 一坨配置,我的代码要这么写么?
var cpuPrice map[string]float {
xxx : 100,
xxxx : 200
}
// 电脑有 N 个部件,我的函数入参就要有 N 个么?
// 这个函数谁敢用?
getMaxPrice(type= '', cpu='',mem='',ram='',disk=''...一堆配置){
if type == 'mac' {
price := 0
if(cpu == '') {
cpu="默认配置"
}
price += cpuPrice[cpu]
if(mem == '') {
mem="默认配置"
}
price += memPrice[mem]
...
} elseif (type == 'huawei') {
...
}
}
这样看这坨代码的代码量绝对高,并且大部分是重复代码,而且当电脑配置越来越多,getMaxPrice 函数入参也跟着变多,参数顺序谁能保证?有的产生是必填有的是非必填,怎么帮助必填的没有被漏掉?
这样的代码时间久了,逻辑看着很简单,但是没人敢用吧。
怎么办呢?
使用生成器模式来解决是不是好一点,每个部件作为一个生成步骤,每次执行一个步骤即添加一个部件配置,最终生成一个完整的电脑报价,并且设置部件、获取折扣、计数报价这些步骤本身是有序的,是可以通过生成器模式中的Director小干部来统一操作的。
好,来看代码吧!
代码"文档"
Tips: 代码 + 注释。
自我要求:注释覆盖率 > 60%
1.先定义一个电脑报价的配置总类,即我们要生成的产品:Computer
package builder
// 产品: 这个是我们的目标,computer 要有这些配置
// computer 可以理解成我们要制作一个什么产品
// 结构体字段 可以理解为我们要做的产品都要哪些配置,对应上文 生成函数的 N 多个入参
type Computer struct {
name string // 电脑类型 比如 mac/华为
cpuModel CPU_MODEL // cpu 型号
gpuModel GPU_MODEL // gpu 型号
memorySize MEM_SIZE // 内存大小
diskSize DISK_SIZE // 磁盘大小
discount float64 // 折扣
price float64 // 整体报价
}
2.再定义一个电脑生成的步骤规范接口
package builder
// 生成器接口: 产品的生成器接口
// 可以理解为这个产品生成 必须要有哪些具体的步骤和行为, 后面每一个抽象的产品生成对象都要继承这个生成器接口
type builder interface {
setCpuModel(CPU_MODEL) // 设置 cpu 型号
setGpuModel(GPU_MODEL) // 设置 gpu 型号
setMemorySize(MEM_SIZE) // 设置 内存型号
setDiskSize(DISK_SIZE) // 设置磁盘型号
setDiscount() // 设置折扣粒度, 这个折扣粒度是系统内置的,不需要客户端设置也就是说此功能不是给前台用户询价时自定义的。
calculatePrice() // 计算报价
getComputer() *Computer // 给主管(director)使用
}
3.开始定义各个电脑品牌抽象生成器
先看 Mac 的
package builder
import (
"time"
)
// 抽象的产品生成器
// 可以理解为computer 这个产品中某一类型产品的生成器
// 抽象生成器即包含了产品(computer)的所有配置,也继承了 builder 公共生成器的所有生成步骤
type MacComputerBuilder struct {
c *Computer
}
// 实力化一个 Mac 电脑报价
func NewMacComputerBuilder() builder {
return &MacComputerBuilder{
c: &Computer{name: "mac"},
}
}
// 返回*Computer
func (mc *MacComputerBuilder) getComputer() *Computer {
return mc.c
}
// 设置 CPU型号
// 设置配置的时候要判断,如果客户端已经配置了,那么跳过
// 这块是因为 director 会在最后编译的时候统一整体执行一遍,防止客户端漏掉配置,走默认配置
func (mc *MacComputerBuilder) setCpuModel(m CPU_MODEL) {
// demo
if mc.c.cpuModel != "" {
return
}
if price, ok := partsCpuPriceMap[m]; ok {
mc.c.cpuModel = m
mc.c.price += price
} else {
mc.c.cpuModel = MAC_CPU_I5 // 此为 mac 电脑默认 cpu 配置
mc.c.price += partsCpuPriceMap[MAC_CPU_I5]
}
}
// 设置 GPU型号
// 设置配置的时候要判断,如果客户端已经配置了,那么跳过
// 这块是因为 director 会在最后编译的时候统一整体执行一遍,防止客户端漏掉配置,走默认配置
func (mc *MacComputerBuilder) setGpuModel(m GPU_MODEL) {
// demo
if mc.c.gpuModel != "" {
return
}
if price, ok := partsGpuPriceMap[m]; ok {
mc.c.gpuModel = m
mc.c.price += price
} else {
mc.c.gpuModel = MAC_GPU_NVIDIA // 此为 mac 电脑默认 gpu 配置
mc.c.price += partsGpuPriceMap[MAC_GPU_NVIDIA]
}
}
// 设置内存大小
// 设置配置的时候要判断,如果客户端已经配置了,那么跳过
// 这块是因为 director 会在最后编译的时候统一整体执行一遍,防止客户端漏掉配置,走默认配置
func (mc *MacComputerBuilder) setMemorySize(s MEM_SIZE) {
// demo
if mc.c.memorySize != "" {
return
}
if price, ok := partsMemPriceMap[s]; ok {
mc.c.memorySize = s
mc.c.price += price
} else {
mc.c.memorySize = MAC_MEM_8G // 此为 mac 电脑默认 内存 配置
mc.c.price += partsMemPriceMap[MAC_MEM_8G]
}
}
// 设置 磁盘大小
// 设置配置的时候要判断,如果客户端已经配置了,那么跳过
// 这块是因为 director 会在最后编译的时候统一整体执行一遍,防止客户端漏掉配置,走默认配置
func (mc *MacComputerBuilder) setDiskSize(s DISK_SIZE) {
// demo
if mc.c.diskSize != "" {
return
}
if price, ok := partsDiskPriceMap[s]; ok {
mc.c.diskSize = s
mc.c.price += price
} else {
mc.c.diskSize = MAC_DISK_500G // 此为 mac 电脑默认 磁盘 配置
mc.c.price += partsDiskPriceMap[MAC_DISK_500G]
}
}
// 设置折扣
// 不同产品策略不一样
// 此操作为内置操作,不需要外部设置
func (mc *MacComputerBuilder) setDiscount() {
// 2021-06-24 00:17:33
// 如果大于这个时间,那么 mac 电脑整体打5折
// 否则 整体打8折
if time.Now().Unix() > 1624465043 {
mc.c.discount = 0.5
} else {
mc.c.discount = 0.8
}
}
// 计数价格
// 注意看,这块就是需要时序的地方,需要先setDiscount 才能进行报价
// 所以 需要通过 指挥者来统一进行构建,保证各个行为执行顺序
func (mc *MacComputerBuilder) calculatePrice() {
mc.c.price = (mc.c.price * mc.c.discount)
}
在看一个huawei 的。
package builder
import "C"
// 抽象的产品生成器
// 可以理解为computer 这个产品中某一类型产品的生成器
// 抽象生成器即包含了产品(computer)的所有配置,也继承了 builder 公共生成器的所有生成步骤
type HuaweiComputerBuilder struct {
c *Computer
}
func NewHuaweiComputerBuilder() builder {
return &HuaweiComputerBuilder{
c: &Computer{name: "huawei"},
}
}
func (hc *HuaweiComputerBuilder) getComputer() *Computer {
return hc.c
}
/**
* 以下设置各个配置方法和 Mac 逻辑一样,当然也可以自定义策略,不过 demo 就这样了,保证篇幅,所以就不写了
*/
// 设置 CPU型号
func (hc *HuaweiComputerBuilder) setCpuModel(m CPU_MODEL) {}
// 设置 GPU型号
func (hc *HuaweiComputerBuilder) setGpuModel(m GPU_MODEL) {}
// 设置内存大小
func (hc *HuaweiComputerBuilder) setMemorySize(s MEM_SIZE) {}
// 设置 磁盘大小
func (hc *HuaweiComputerBuilder) setDiskSize(s DISK_SIZE) {}
// 设置优惠折扣,这块是内部逻辑,不需要外部调用方定义,而且不同产品策略不一样
func (hc *HuaweiComputerBuilder) setDiscount() {
// 华为机器不打折,国产赞。 这块就是和 mac 差异化的地方
hc.c.discount = 1
}
// 既然华为不打折,那么直接输出就好了
func (hc *HuaweiComputerBuilder) calculatePrice() {
}
看到区别了吧,两个品牌生成器的优惠策略不同
,计数价格方法不同
,但是统一生成步骤一样
,所以需要主管
来统一调度执行
来看主管director
package builder
// director 主管,负责整体 build 执行
// 可以理解为总指挥,他来负责计算报价
type director struct {
builder builder
}
// 实例化一个主管
func NewDirector(b builder) *director {
return &director{
builder: b,
}
}
// 手动重置主管,方便进行多次不同产品生成构建
func (d *director) resetBuilder(b builder) {
d.builder = b
}
// 执行编译生成,这块就是要严格统一管理编译的步骤和顺序
// 当前这个 demo , 因为时计算报价的例子而不是生成电脑配置的例子,所以前置的那些 setXXX 都在客户端自定义执行了
// 但是有可能前台用户没有选择某些配置,所以需要主管统一兜底
// 1. 兜底每个电脑配置
// 2. 根据当前时间选择折扣粒度
// 3. 计算报价
func (d *director) buildComputer() *Computer {
// 第一步,兜底每一个电脑配置
d.builder.setCpuModel(DIRECTOR_CHECK_PARAMS)
d.builder.setGpuModel(DIRECTOR_CHECK_PARAMS)
d.builder.setMemorySize(DIRECTOR_CHECK_PARAMS)
d.builder.setDiskSize(DIRECTOR_CHECK_PARAMS)
// 第二步设置折扣
d.builder.setDiscount()
// 第三步 计算报价
d.builder.calculatePrice()
// 返回产品对象
return d.builder.getComputer()
}
到这块是不是差不多看懂了?最后我们看下客户端是如何调用实现的:
package builder
import "fmt"
// 客户端询问报价
// 即用户在前台页面选择了 mac 电脑
// CPU i7
// GPU xxx
func getPrice() {
// 先实例化抽象生成器对象,即 mac 电脑
mcb := NewMacComputerBuilder()
// 设置我想询问的配置
mcb.setCpuModel(MAC_CPU_I7)
mcb.setGpuModel(MAC_GPU_NVIDIA)
mcb.setMemorySize(MAC_MEM_16G)
// 磁盘我不选了,用默认的
//mcb.setDiskSize()
// 然后实例化一个主管,来准备生成报价
d := NewDirector(mcb)
// 执行编译,生成最终产品
product := d.buildComputer()
// ok 搞定了,我们可以看看最终这个产品的配置和报价
fmt.Printf("current computer name: %s\\n", product.name)
fmt.Printf("choose config cpuModel: %s\\n", product.cpuModel)
fmt.Printf("choose config gpuModel: %s\\n", product.gpuModel)
fmt.Printf("choose config memorySize: %s\\n", product.memorySize)
fmt.Printf("choose config diskSize: %s\\n", product.diskSize)
fmt.Printf("give you discount: %f\\n", product.discount)
fmt.Printf("final offer: %f\\n", product.price)
fmt.Printf("---------------询问下一个电脑---------------\\n")
// 下面 我们再生成一个华为的电脑报价
hwcb := NewHuaweiComputerBuilder()
hwcb.setCpuModel(HW_CPU_I7)
hwcb.setGpuModel(HW_GPU_ATI)
hwcb.setMemorySize(HW_MEM_16G)
hwcb.setDiskSize(HW_DISK_1T)
d.resetBuilder(hwcb)
// 执行编译,生成最终产品
product2 := d.buildComputer()
// ok 搞定了,我们可以看看最终这个产品的配置和报价
fmt.Printf("current computer name: %s\\n", product2.name)
fmt.Printf("choose config cpuModel: %s\\n", product2.cpuModel)
fmt.Printf("choose config gpuModel: %s\\n", product2.gpuModel)
fmt.Printf("choose config memorySize: %s\\n", product2.memorySize)
fmt.Printf("choose config diskSize: %s\\n", product2.diskSize)
fmt.Printf("give you discount: %f\\n", product2.discount)
fmt.Printf("final offer: %f\\n", product2.price)
}
上线效果
=== RUN TestGetPrice
current computer name: mac
choose config cpuModel: maci7
choose config gpuModel: mac-NVIDIA
choose config memorySize: mac-16g
choose config diskSize: mac-500g
give you discount: 0.500000
final offer: 600.000000
---------------询问下一个电脑---------------
current computer name: huawei
choose config cpuModel: hwi7
choose config gpuModel: hw-ATI
choose config memorySize: hw-16g
choose config diskSize: hw-1t
give you discount: 1.000000
final offer: 2800.000000
--- PASS: TestGetPrice (0.00s)
PASS
还是符合预期的!
Demo源码
:https://github.com/xiaoxuz/design-pattern/tree/main/create/builder
生成器优缺点
- 优点
- 你可以分步创建对象,
暂缓
创建步骤或递归运行
创建步骤。 - 生成不同形式的产品时, 你可以
复用相同的制造代码
。 *单一职责原则*
。 你可以将复杂构造代码从产品的业务逻辑中分离出来。
- 你可以分步创建对象,
- 缺点
- 由于该模式需要新增多个类, 因此代码整体
复杂程度
会有所增加。
- 由于该模式需要新增多个类, 因此代码整体
思考
认识我们的职业,不是码农,是软件工程师!
收工
打完收工,感谢阅读!
【点击】关注再看,您的关注是我前进的动力~!
以上是关于生成器模式-代码的艺术系列的主要内容,如果未能解决你的问题,请参考以下文章
Azure 机器人微软Azure Bot 编辑器系列 : 机器人/用户提问回答模式,机器人从API获取响应并组织答案 (The Bot Framework Composer tutorial(代码片段