首页 > 其他分享 >浅析建造者模式

浅析建造者模式

时间:2023-07-16 17:13:27浏览次数:33  
标签:vb return int house Builder 建造 模式 func 浅析


0. 前言

建造者模式是创建型设计模式的一种。本篇文章将介绍什么是建造者模式,以及什么时候用建造者模式,同时给出 Kubernetes:kubectl 中类似建造者模式的示例以加深理解。

1. 建造者模式

1.1 从工厂函数说起

试想构建房子类,其属性如下:

type house struct {
	window   int
	door     int
	bed      int
	desk     int
	deskLamp int
}

其中,door, windowbed 是必须配置,deskdeskLamp 是可选配置,且 deskdeskLamp 是配套配置。

通过工厂函数创建 house 对象:

func NewHouse(window, door, bed, desk, deskLamp int) *house {
	return &house{
		window:   window,
		door:     door,
		bed:      bed,
		desk:     desk,
		deskLamp: deskLamp,
	}
}

这里有个问题在于 deskdeskLamp 是可选配置。通过 NewHouse 创建对象需要指定 deskdeskLamp

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)

看起来还不错。

不过 deskdeskLamp 要配套出现,这里并没有检查配套的逻辑。并且,window, doorbed 需要检测,如果传入的是 0 或 负数,应该要报错。

结合这两点,继续构建 house 对象。构建有两种思路:思路一,在构造完的 house 对象上添加 validation 方法校验属性。思路二,在工厂函数内校验必配属性,新建方法检查 deskdeskLamp 是否配套出现。

这两种思路虽然能实现校验功能,但是都有瑕疵。
思路一,在构造完对象后才验证,如果对象忘了调用 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 的属性,houseBuilder 造的,作为属性挺合理的。
2)重命名 Validationbuild,之所以这么命名是想说明 build 是创建的最后一步,结束 build 之后即可获得 house 对象。

对于调用方,创建对象就变成了:

house, _ := NewBuilder().SetRequisite(2, 1, 1).SetDesk(1).SetDeskLamp(1).build()

这里 deskdeskLamp 是配套使用的,如果不需要的话。创建对象就变成:

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()

可以看到,通过 Builderbuild 方法实现了创建过程的封装,对于调用方来说相当友好。

继续往下分析,刚才的参数是固定的。如果要用户可配,而不是内定的参数。怎么做呢?

重新改造代码如下:

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, doorchair。通过 Builer 内的属性将参数传给内嵌产品对象,实现有序创建。

参数可配带来的问题在于,可以整合 villaBuilderresidenceBuilder 为一个 Builder。通过该 Builder 实现根据不同配置创建 house
那就蜕化为前面的 示例 1.1 的实现了。

试想,这时候在新增冰箱和饮料两个属性,且这两个属性是可选的,配套的。那么怎么创建 housecar 呢?

同样的道理,将可选项赋值给 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 负责整体的顺序创建,可以把这块逻辑向上提给 DirectorBuilder 只关心部件的创建,而不需要关心整体。做到逻辑的进一步拆分。代码示例如下:

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
}

调用方只需要创建 BuilderDirector 而不需要关心实现细节。

画建造者模式的 UML 图,最后感受下:

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 非常像。通过 factoryNewBuilder 创建 Builder,接着通过一系列建造者方法构造 Builder,最后构建完成的 Builder 调用 Do 方法创建 resouce.Result 对象。

2. 小结

从上述分析可以做个建造者模式的小结:
1) 建造者模式是表达和实现分离,对于调用方来说不需要关注细节实现。
2) 建造者模式其内部对象建造顺序是稳定的,实现是复杂的。摘录《设计模式之美》的一段话表明什么时候该用建造者模式:

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

3) 建造者模式建造的对象是可用的,安全的。


标签:vb,return,int,house,Builder,建造,模式,func,浅析
From: https://www.cnblogs.com/xingzheanan/p/17558133.html

相关文章

  • VMware ubuntu网卡桥接模式配置
    仅对以下环境试验VMware16.0.0build-16894299ubuntu20.04开发板:s5p6818一个fast家用路由器连接:一根网线连开发板,一根网线连路由器lan口和主机,一根网线连笔记本和路由器lan口为什么笔记本不用无线网卡?因为路由器没连网,我要连internet,无线网卡连手机热点。试验过程:连接好......
  • 设计模式-法则大全
    SOLID原则:单一职责原则SRP:一个类只负责完成一个职责或功能;要设计粒度小、功能单一的类开闭原则OCP:对扩展开放、对修改关闭;在已有基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等);里式替换LSP:父类定义了函数的“约定”(或者协议),那子类可以改变函数的内......
  • 02-设计模式-观察者模式
    观察者模式涉及的对象:观察者接口、观察者接口的实现类被观察者接口、被观察者接口的实现类1、观察者接口-代码:publicinterfaceObserver{voidupdate(Stringmsg);}2、观察者接口的实现类-代码:publicclassObserverImplimplementsObserver{privateSt......
  • 01-设计模式-代理模式
    1、代理模式的分类代理模式分为:静态代理:在编译阶段确定了被代理对象的类型,简单。动态代理:在运行阶段确定了被代理对象的类型,复杂。2、静态代理静态代理涉及的类:一个接口,下面的例子中命名为Subject实现了接口的被代理对象RealSubject实现了接口的代理对象StaticProxy......
  • 职责链模式-15
    概述职责链模式(ChainofResponsibilityPattern)又称责任链模式。它将请求的发送者和接收者解耦,如果有多个接收者,将这些接收者连接成一条链,请求沿着这条链传递,直到被处理。优点:降低耦合度,符合“开闭原则”。缺点:请求不一定会被处理。链过长会造成性能问题。classRequest......
  • 145.观察者模式和发布订阅模式有什么不同
    145.观察者模式和发布订阅模式有什么不同?发布订阅模式其实属于广义上的观察者模式在观察者模式中,观察者需要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并作出响应。而在发布订阅模式中,发布者和订阅者之间多了一个调度中心。调度中心一方面从发布者接收事件,......
  • c语言中的大端模式和小端模式
    在学习共用体(联合体union)的时候,碰到了非常经典的大小端模式问题,以下是个人的见解:大端模式与小端模式所有运算的操作数,必须在CPU的內部寄存器才能参与运算对于CPU来说,寄存器才是它真正的储存空间,但是CPU的寄存器的个数以及容量是非常有限的,所以在设计CPU时,必须要有一个它的存储......
  • kubernetes网络模式
     一个K8s的集群中至少有三个网络:集群节点所在的网络,这个网络就是你的主机所在的网络,通常情况下是你的网络基础设施提供。如果你的node处于不同的网段,那么你需要保证路由可达。如上图中的192.168.10.0/24和10.0.0.0/8这两个网络第二个网络是Pod的网络,K8s中一个Pod由多个......
  • KingbaseES V8R6集群运维案例之---single-pro模式备份
    案例说明:KingbaseESV8R6集群物理备份配置参数_target_db_style,可选single或cluster或single-pro。single对应单机模式的目标数据库实例,cluster对应集群模式的目标数据库实例,single-pro对应集群模式的每个DB节点独立备份。本案例详细描述集群架构在singl-pro模式下的备份。适用......
  • mongodb4.4.22主从(副本集附仲裁节点)部署带认证模式
    环境:OS:CentOS7DB:4.4.22机器角色:192.168.1.102:29001主192.168.1.104:29001从192.168.1.105:29001仲裁节点 1.下载相应的版本https://www.mongodb.com/download-center/community我这里下载的是mongodb-linux-x86_64-rhel70-4.4.22.tgz 2.创建安装目录192.168.1.102......