浅析建造者模式 ╰+哭是因爲堅強的太久メ 2024-03-17 21:13 54阅读 0赞 建造者模式是创建型设计模式的一种。本篇文章将介绍什么是建造者模式,以及什么时候用建造者模式,同时给出 `Kubernetes:kubectl` 中类似建造者模式的示例以加深理解。 ## 1. 建造者模式 ## ### 1.1 从工厂函数说起 ### 试想构建房子类,其属性如下: type house struct { window int door int bed int desk int deskLamp int } 其中,`door`, `window` 和 `bed` 是必须配置,`desk` 和 `deskLamp` 是可选配置,且 `desk` 和 `deskLamp` 是配套配置。 通过工厂函数创建 `house` 对象: func NewHouse(window, door, bed, desk, deskLamp int) *house { return &house{ window: window, door: door, bed: bed, desk: desk, deskLamp: deskLamp, } } 这里有个问题在于 `desk` 和 `deskLamp` 是可选配置。通过 `NewHouse` 创建对象需要指定 `desk` 和 `deskLamp`: house := NewHouse(2, 1, 1, 0, 0) 这对调用者来说不必要。 继续,使用 `set` 结合工厂函数构造 `house` 对象: func NewHouse(window, door, bed int) *house { return &house{ window: window, door: door, bed: bed, } } func (h *house) SetDesk(desk int) { h.desk = desk } func (h *house) SetDeskLamp(deskLamp int) { h.deskLamp = deskLamp } 创建 `house` 对象: house := NewHouse(2, 1, 1) // 使用 set 设置 desk 和 deskLamp house.SetDesk(1) house.SetDeskLamp(1) 看起来还不错。 不过 `desk` 和 `deskLamp` 要配套出现,这里并没有检查配套的逻辑。并且,`window`, `door` 和 `bed` 需要检测,如果传入的是 0 或 负数,应该要报错。 结合这两点,继续构建 `house` 对象。构建有两种思路:思路一,在构造完的 `house` 对象上添加 `validation` 方法校验属性。思路二,在工厂函数内校验必配属性,新建方法检查 `desk` 和 `deskLamp` 是否配套出现。 这两种思路虽然能实现校验功能,但是都有瑕疵。 思路一,在构造完对象后才验证,如果对象忘了调用 `validation`,那这个对象就是个不安全的对象。 思路二,校验分开了,对象的属性应该放在一起校验,试想如果参数过多,且相互有依赖关系,那又得新增方法判断,麻烦且容易出错。 并且,对于调用方来说,构造过程暴露太多了。工厂函数的优势在于调用方无感知,如果暴露太多 set 方法,并且由调用方来调用验证方法验证对象属性。那工厂函数的优势将大打折扣。 ### 1.2 工厂函数到建造者的优雅过渡 ### 如何适配上述场景,使得调用方无感知呢? 试拆分上述代码如下: func NewHouse() *house { return &house{} } func (h *house) SetRequisite(window, door, bed int) *house { h.window = window h.door = door h.bed = bed return h } func (h *house) SetDesk(desk int) *house { h.desk = desk return h } func (h *house) SetDeskLamp(deskLamp int) *house { h.deskLamp = deskLamp return h } func (h *house) Validation() (*house, error) { if h.window <= 0 || h.door <= 0 || h.bed <= 0 { return nil, errors.New("invalid [window|door|bed]") } if h.desk < 0 || h.deskLamp < 0 { return nil, errors.New("invalid [desk|deskLamp]") } if !(h.desk > 0 && h.deskLamp > 0) { return nil, errors.New("need desk and deskLamp at same time") } return h, nil } 创建 `house` 对象: house, _ := NewHouse().SetRequisite(2, 1, 1).SetDesk(1).SetDeskLamp(1).Validation() 嗯,看起来清晰了不少。不过我们细细分析下逻辑的话还是会发现那么一点怪异的点。这一点在于,`house` 对象是 `set` 的主体,这在逻辑上好像不通。 是的,我们需要引入一个新对象叫 `Builder` 来创建 `house`,而不是让 `house` 自己创建自己。 改造代码如下: *示例 1.1* type Builder struct { house } func NewBuilder() *Builder { return &Builder{} } func (b *Builder) SetRequisite(window, door, bed int) *Builder { b.window = window b.door = door b.bed = bed return b } func (b *Builder) SetDesk(desk int) *Builder { b.desk = desk return b } func (b *Builder) SetDeskLamp(deskLamp int) *Builder { b.deskLamp = deskLamp return b } func (b *Builder) build() (*house, error) { if b.window <= 0 || b.door <= 0 || b.bed <= 0 { return nil, errors.New("invalid [window|door|bed]") } if b.desk < 0 || b.deskLamp < 0 { return nil, errors.New("invalid [desk|deskLamp]") } if !(b.desk > 0 && b.deskLamp > 0) { return nil, errors.New("need desk and deskLamp at same time") } return &b.house, nil } 这里做了几点改动: 1)新建 `Builder` 对象,通过 `Builder` 对象创建 `house`。并且,将 `house` 作为 `Builder` 的属性,`house` 是 `Builder` 造的,作为属性挺合理的。 2)重命名 `Validation` 为 `build`,之所以这么命名是想说明 `build` 是创建的最后一步,结束 `build` 之后即可获得 `house` 对象。 对于调用方,创建对象就变成了: house, _ := NewBuilder().SetRequisite(2, 1, 1).SetDesk(1).SetDeskLamp(1).build() 这里 `desk` 和 `deskLamp` 是配套使用的,如果不需要的话。创建对象就变成: house, _ := NewBuilder().SetRequisite(2, 1, 1).build() 要留意这种结构,它是顺序不一致的。 如果顺序一致的情况,即创建的流程都是一样的。那么可以将 `build` 抽象为接口,使用不同的接口创建产品,且创建的产品流程是一样的,可以用封装将这一过程封装起来。 举例,使用两个 `Builder` 创建房子。`villaBuilder` 先建十个门,再建五十个窗,最后放五十把椅子。`residenceBuilder` 负责建两个门,两个窗,以及五把椅子。代码如下: type Builder interface { createDoor() Builder createWindow() Builder createChair() Builder build() (*house, error) } type villaBuilder struct { house } type residenceBuilder struct { house } type house struct { door int window int chair int } func (vb *villaBuilder) createDoor() Builder { vb.door = 10 return vb } func (vb *villaBuilder) createWindow() Builder { vb.window = 50 return vb } func (vb *villaBuilder) createChair() Builder { vb.chair = 50 return vb } func (vb *villaBuilder) validation() error { return nil } func (vb *villaBuilder) build() (*house, error) { // validate property of object houseBuilder, skip... err := vb.validation() vb.createDoor() vb.createWindow() vb.createChair() return &vb.house, err } func (rb *residenceBuilder) createDoor() Builder { rb.door = 2 return rb } func (rb *residenceBuilder) createWindow() Builder { rb.window = 2 return rb } func (rb *residenceBuilder) createChair() Builder { rb.chair = 1 return rb } func (rb *residenceBuilder) validation() error { return nil } func (rb *residenceBuilder) build() (*house, error) { // validate property of object carBuilder, skip... err := rb.validation() rb.createDoor() rb.createWindow() rb.createChair() return &rb.house, err } func NewBuilder(typ string) Builder { switch typ { case "villa": return &villaBuilder{} case "residence": return &residenceBuilder{} default: return nil } } 最后,通过不同类型的 Builder 创建房子: house, err := NewBuilder("villa").build() 可以看到,通过 `Builder` 的 `build` 方法实现了创建过程的封装,对于调用方来说相当友好。 继续往下分析,刚才的参数是固定的。如果要用户可配,而不是内定的参数。怎么做呢? 重新改造代码如下: type villaBuilder struct { house window int door int chair int } func (hb *villaBuilder) createDoor(door int) Builder { hb.house.door = door return hb } func (hb *villaBuilder) createWindow(window int) Builder { hb.house.window = window return hb } func (hb *villaBuilder) createChair(chair int) Builder { hb.house.chair = chair return hb } func (hb *villaBuilder) build() (*house, error) { // validate property of object villaBuilder, skip... err := hb.validate() hb.createDoor(hb.door) hb.createWindwo(hb.window) hb.createChair(hb.chair) return hb.car, err } func NewBuilder(typ string) Builder { switch typ { case "villa": return &villaBuilder{} case "residence": return &residenceBuilder{} default: return nil } } 调用方创建 `house`: house, err := NewBuilder("house", 2, 2, 2).build() 这里最大的改变在于 `Builder` 对象中新增可配置属性 `window`, `door` 和 `chair`。通过 `Builer` 内的属性将参数传给内嵌产品对象,实现有序创建。 参数可配带来的问题在于,可以整合 `villaBuilder` 和 `residenceBuilder` 为一个 `Builder`。通过该 `Builder` 实现根据不同配置创建 `house`。 那就蜕化为前面的 *示例 1.1* 的实现了。 试想,这时候在新增冰箱和饮料两个属性,且这两个属性是可选的,配套的。那么怎么创建 `house` 和 `car` 呢? 同样的道理,将可选项赋值给 `Builder` 中的属性。代码如下: *示例 1.2* type villaBuilder struct { house window int door int chair int icer int drink int } func (vb *villaBuilder) createIcer() Builder { vb.house.icer = vb.icer return vb } func (vb *villaBuilder) createDrink() Builder { vb.house.drink = vb.drink return vb } func (vb *villaBuilder) setIcer(icer int) Builder { vb.icer = icer return vb } func (hb villaBuilder) setDrink(drink int) Builder { vb.drink = drink return vb } 调用方创建过程为: house, err := NewBuilder("house", 2, 2, 2).setIcer(1).setDrink(1).build() 调用方一直在和 `Builder` 打交道。可选配置传递给 `Builder`,最后通过 `build` 创建出 `house`,做到了表达和实现分离。 ### 1.3 建造者模式 ### 讲到这里基本也差不多了,在建造者模式中还有个 `Director` 对象作为更上层的封装。 从上面代码示例中,`Builder` 负责整体的顺序创建,可以把这块逻辑向上提给 `Director`,`Builder` 只关心部件的创建,而不需要关心整体。做到逻辑的进一步拆分。代码示例如下: type Director struct { builder Builder } func (d *Director) createHouse() (*house, error) { if err := d.builder.validation(); err != nil { return nil, err } d.builder.createDoor() d.builder.createWindwo() d.builder.createChair() return *hb.house, nil } 调用方只需要创建 `Builder` 和 `Director` 而不需要关心实现细节。 画建造者模式的 UML 图,最后感受下: ![96dceadd589c1c11d365eca0ff38f45c.png][] ### 1.4 建造者模式在 Kubernetes:kubectl 的应用 ### 在 `kubectl` 上找到了建造者模式的应用,虽然不是“完全体”,不过没有关系。代码如下: // https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/get/get.go r := f.NewBuilder(). Unstructured(). NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces). FilenameParam(o.ExplicitNamespace, &o.FilenameOptions). LabelSelectorParam(o.LabelSelector). FieldSelectorParam(o.FieldSelector). Subresource(o.Subresource). RequestChunksOf(chunkSize). ResourceTypeOrNameArgs(true, args...). ContinueOnError(). Latest(). Flatten(). TransformRequests(o.transformRequests). Do() 这段代码是不是和我们的示例 1.2 非常像。通过 `factory` 的 `NewBuilder` 创建 `Builder`,接着通过一系列建造者方法构造 `Builder`,最后构建完成的 `Builder` 调用 `Do` 方法创建 `resouce.Result` 对象。 ### 2. 小结 ### 从上述分析可以做个建造者模式的小结: 1) 建造者模式是表达和实现分离,对于调用方来说不需要关注细节实现。 2) 建造者模式其内部对象建造顺序是稳定的,实现是复杂的。摘录《设计模式之美》的一段话表明什么时候该用建造者模式: 顾客走进一家餐馆点餐,我们利用工厂模式,根据顾客不同的选择,制作不同的食物,如比萨、汉堡和沙拉等。对于比萨,顾客又有各种配料可以选择,如奶酪、西红柿和培根等。我们通过建造者模式,根据顾客选择的不同配料,制作不同口味的比萨。 3) 建造者模式建造的对象是可用的,安全的。 [96dceadd589c1c11d365eca0ff38f45c.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/03/14/cc2da98dd9cb4da9a0d04eb76897f3fb.png
相关 浅析建造者模式 建造者模式是创建型设计模式的一种。本篇文章将介绍什么是建造者模式,以及什么时候用建造者模式,同时给出 `Kubernetes:kubectl` 中类似建造者模式的示例以加深理解 ╰+哭是因爲堅強的太久メ/ 2024年03月17日 21:13/ 0 赞/ 55 阅读
相关 建造者模式 定义 将一个复杂对象的构造与它的表示分离,是的同样的构建过程可以创建不同的表示。 类型:创建型 建造者模式与工厂模式有些类似: 建造者更注重:方法的调用顺序,关心创 「爱情、让人受尽委屈。」/ 2022年02月15日 19:25/ 0 赞/ 256 阅读
相关 建造者模式 [2019独角兽企业重金招聘Python工程师标准>>> ][2019_Python_] ![hot3.png][] 1、产品类 package com.hhdys r囧r小猫/ 2022年01月14日 22:37/ 0 赞/ 221 阅读
相关 建造者模式 建造者模式,又称为生成器模式,是一种较为复杂、使用频率不高的创建型模式。它为客户端返回的不是一个简单的产品,而是有多个部件组成的复杂产品。以手机为例,建造者模式返回的是一个完整 谁践踏了优雅/ 2021年12月10日 18:29/ 0 赞/ 226 阅读
相关 建造者模式 建造者模式 建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 柔光的暖阳◎/ 2021年09月29日 16:04/ 0 赞/ 299 阅读
相关 建造者模式 建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 一个 Buil 向右看齐/ 2021年09月17日 01:24/ 0 赞/ 343 阅读
相关 建造者模式 4.建造者模式 ![70][] class Customer { static void Main(string[] a 女爷i/ 2021年09月16日 23:52/ 0 赞/ 348 阅读
相关 建造者模式 建造者模式 1. 简介 2. 建造者模式介绍 2.1 定义 2.2 建造者模式的优点 2.3 建造者模式的缺点 ﹏ヽ暗。殇╰゛Y/ 2021年08月30日 22:18/ 0 赞/ 474 阅读
相关 建造者模式 一 点睛 将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naG 怼烎@/ 2021年07月24日 18:01/ 0 赞/ 400 阅读
相关 建造者模式 造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 一个 Bu... 小灰灰/ 2020年06月13日 05:36/ 0 赞/ 723 阅读
还没有评论,来说两句吧...