生成器模式-代码的艺术系列

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.
将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示

概括的说:有些对象的创建流程是一样的,但是因为自身特性的不同,所以在创建他们的时候需要将创建过程个性化的属性分离出来。

基本看不懂啥意思,! 继续看结构~

来看结构:

  1. 生成器 (Builder) 接口声明在所有类型生成器中通用的产品构造步骤。
  2. 具体生成器 (Concrete Builders) 提供构造过程的不同实现。 具体生成器也可以构造不遵循通用接口的产品。
  3. 产品 (Products) 是最终生成的对象。 由不同生成器构造的产品无需属于同一类层次结构或接口。
  4. 主管 (Director) 类定义调用构造步骤的顺序, 这样你就可以创建和复用特定的产品配置。
  5. 客户端 (Client) 必须将某个生成器对象与主管类关联。 一般情况下, 你只需通过主管类构造函数的参数进行一次性关联即可。 此后主管类就能使用生成器对象完成后续所有的构造任务。

在这里插入图片描述


实战学习

概念性描述你懂了么?不懂就对了,程序员先理解概念不如直接上代码来的刺激。

这里贯彻下我们前言中的宗旨:

  1. 不要生活场景的举例,要产品需求的案例。
  2. 不要纯图文的描述,要可阅读的代码。

上实战~

产品需求

没有产品思维的程序员不是好销售,此需求纯属虚构

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

生成器优缺点

  • 优点
    • 你可以分步创建对象,暂缓创建步骤或递归运行创建步骤。
    • 生成不同形式的产品时, 你可以复用相同的制造代码
    • *单一职责原则*。 你可以将复杂构造代码从产品的业务逻辑中分离出来。
  • 缺点
    • 由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。

思考

认识我们的职业,不是码农,是软件工程师!

在这里插入图片描述

收工

打完收工,感谢阅读!

【点击】关注再看,您的关注是我前进的动力~!

在这里插入图片描述

以上是关于生成器模式-代码的艺术系列的主要内容,如果未能解决你的问题,请参考以下文章

生成器模式-代码的艺术系列

开发成长之路(14)-- 设计模式:写代码的艺术

开发成长之路(14)-- 设计模式:写代码的艺术

Azure 机器人微软Azure Bot 编辑器系列 : 机器人/用户提问回答模式,机器人从API获取响应并组织答案 (The Bot Framework Composer tutorial(代码片段

Android开发探索艺术学习笔记

单元测试的艺术 ---- 系列文章