从“汽车制造”生活案例到软件的建造者模式

举报
宇宙之一粟 发表于 2022/10/27 21:37:47 2022/10/27
【摘要】 0 生活案例沙师弟 : “大师兄,车是怎么建成的啊?”大师兄:“从外部看,车由车身、座椅和轮胎,从内部又有引擎、方向盘、电路系统、刹车系统、冷却系统等等组成,这些复杂的部件一般都不是一个厂商来完成的,而是将这些交付给汽车零部件制造商。不同的生产商来最终完成不同部件的生产,采购完整个零部件,最后在车间完成整个组装。”汽车这个复杂的对象就可以通过建造者模式来将部件和组装过程分开,帮我们快速完成汽...

laptop-g538cd0443_1920.jpg

0 生活案例

沙师弟 : “大师兄,车是怎么建成的啊?”


大师兄:“从外部看,车由车身、座椅和轮胎,从内部又有引擎、方向盘、电路系统、刹车系统、冷却系统等等组成,这些复杂的部件一般都不是一个厂商来完成的,而是将这些交付给汽车零部件制造商。不同的生产商来最终完成不同部件的生产,采购完整个零部件,最后在车间完成整个组装。”


汽车这个复杂的对象就可以通过建造者模式来将部件和组装过程分开,帮我们快速完成汽车的建造。


1 建造者模式

构建者模式帮助我们构建复杂的对象,而不需要直接实例化它们的结构,或编写它们所需的逻辑。想象一下,一个对象可能有几十个字段,它们本身就是比较复杂的结构。


现在,你有许多具有这些特征的对象,还可以有更多。我们不希望在包中写创建所有这些对象的逻辑,而只在需要使用这些对象的地方写好。


1.1 Go 中的对象实例

在 Go 语言中,实例的创建可以很简单,比如只是简单提供 {} ,然后让实例的值为零;也可以很复杂,比如一个对象需要进行一些 API 调用,检查状态,并为其字段创建对象。


你也可以有一个由多个对象组成的对象,这在 Go 中是非常常见的,因为 Go 不支持继承。


同时,你可以用同样的技术来创造许多类型的对象。例如,你将使用几乎相同的技术来建造一辆汽车和一辆公共汽车,只是它们的尺寸和座位数不同,所以我们为什么不重复使用建造过程呢?这就是建造者模式的用武之地了。


1.2 建造者模式的优点

  • 对复杂的创建进行抽象,以便将对象与对象的使用者进行分开

  • 通过填入字段和创建嵌入对象,一步步创建对象

  • 能够在许多对象之间重复使用对象创建方法


1.3 交通工具制造的例子

建造者模式通常被描述为一个主管 director、几个 Builder 和他们所创建的产品之间的关系。


我们来看关于汽车的例子,创建一个车辆建造器 Builder,建造车(Product)的过程或许会有一些差异,但对每一种车辆来说,整体的过程可以归纳为如下步骤:


  1. 选择车辆类型

  2. 组装结构

  3. 安装车轮

  4. 放置座椅


如果你仔细思考,你可以重复通过这个步骤描述建造一辆骑车和一辆摩托车,在接下来的例子中,主管 director 的角色就是用 Manufacturing 变量进行表示。


1.4 设计思路

正如上述的描述那样,我们必须处理一些 Builder 变量和一个独立的 director . 主管 director 来领导实际建造者 Builder 的建造产品的过程。因此,对于一个车辆建造者的要求是:


  • 需要有一个制造对象来制造交通工具的一切

  • 当使用汽车建造者 Builder 时,返回的车辆产品必须带有 4 个轮子、5 个座椅和定义为 Car 的结构体

  • 使用摩托车建造者时,返回的车辆产品必须带有 2 个轮子、供 2 个人的座位和定义为 Motorbike 的结构体

  • 任何 BuilderProcess 建造者必须开放对车辆产品的修改功能


结构图如下:


See the source image

2 测试驱动开发

根据前文的设计过程,我们将建造一个 director 变量:ManufacturingDirector , 以使用由汽车和摩托车产品建造器的建造过程。dierctor 是负责人,builder 是实际的建造者。


2.1 Builder 接口声明

Builder 声明如下:

package creational

type BuildProcess interface {
  SetWheels() BuildProcess
  SetSeats() BuildProcess
  SetStructure() BuildProcess
  GetVehicle() VehicleProduct
}


  • BuildProcess 接口定义了建造车辆所需的步骤,因此,各个车辆的 Builder 必须实现这个接口。

  • 在每个 SetXXX() 的函数,返回每一个构建的过程,然后将各个步骤连接起来,返回一个 GetVehicle() 的方法。


2.2 Director 主管接口

ManufacturingDirector 主管接口可以来自接收不同的 Builder:

  • 然后有一个 Construct() 方法使用 Builder 来重复建造过程,后面会实现这个方法

  • SetBuilder() 方法用于更换不同的 Builder

// 制作主管
type ManufacturingDirector struct{}

func (f *ManufacturingDirector) Construct() {
	// 建筑过程
}

func (f *ManufacturingDirector) SetBuilder(b BuildProcess) {
	// 选择建造者
}


2.3 Product 产品结构体

产品是我们在制造出的最终对象。在上面的简易例子中,我们假设一辆交通工具是由车轮、座椅和结构组成的。

// 产品
type VehicleProduct struct {
	Wheels    int
	Seats     int
	Structure string
}


2.4 Builder 具体建造者

第一个 Builder 为 Car 建造者 Builder,需要我们实现定义在 BuildProcess 接口的方法:

// 汽车建造者
type CarBuilder struct{}

func (c *CarBuilder) SetWheels() BuildProcess {
	return nil
}

func (c *CarBuilder) SetSeats() BuildProcess {
	return nil
}

func (c *CarBuilder) SetStructure() BuildProcess {
	return nil
}

func (c *CarBuilder) GetVehicle() VehicleProduct {
	return VehicleProduct{}
}


同理,摩托车建造者如下:

// 摩托车建造者
type MotorBuilder struct{}

func (m *MotorBuilder) SetWheels() BuildProcess {
	return nil
}

func (m *MotorBuilder) SetSeats() BuildProcess {
	return nil
}

func (m *MotorBuilder) SetStructure() BuildProcess {
	return nil
}

func (m *MotorBuilder) GetVehicle() VehicleProduct {
	return VehicleProduct{}
}



最终,我们得到完整的 creational.go 文件:

package creational

// 建造过程
type BuildProcess interface {
  SetWheels() BuildProcess
  SetSeats() BuildProcess
  SetStructure() BuildProcess
  GetVehicle() VehicleProduct
}

// 制作主管
type ManufacturingDirector struct{}

func (f *ManufacturingDirector) Construct() {
  // 等待实现
}

func (f *ManufacturingDirector) SetBuilder(b BuildProcess) {
  // 等待实现
}

// 产品
type VehicleProduct struct {
  Wheels    int
  Seats     int
  Structure string
}

// 汽车建造者
type CarBuilder struct{}

func (c *CarBuilder) SetWheels() BuildProcess {
  return nil
}

func (c *CarBuilder) SetSeats() BuildProcess {
  return nil
}

func (c *CarBuilder) SetStructure() BuildProcess {
  return nil
}

func (c *CarBuilder) GetVehicle() VehicleProduct {
  return VehicleProduct{}
}

// 摩托车建造者
type MotorBuilder struct{}

func (m *MotorBuilder) SetWheels() BuildProcess {
  return nil
}

func (m *MotorBuilder) SetSeats() BuildProcess {
  return nil
}

func (m *MotorBuilder) SetStructure() BuildProcess {
  return nil
}

func (m *MotorBuilder) GetVehicle() VehicleProduct {
  return VehicleProduct{}
}


2.5 编写测试用例

针对上面编写的建造过程,我们可以进行如下的测试,同目录下创建 creational_test.go 文件。


1、首先是测试汽车建造过程,假定最终生产的汽车是具有 4 个轮子,5 个座位,然后结构是 Car 类型,写入如下代码:

package creational

import "testing"

func TestBuilderPattern(t *testing.T) {

  manufacturingComplex := ManufacturingDirector{}

  carBuilder := &CarBuilder{}
  manufacturingComplex.SetBuilder(carBuilder)
  manufacturingComplex.Construct()

  car := carBuilder.GetVehicle()

  if car.Wheels != 4 {
    t.Errorf("Wheels on a car must be 4 and they were %d\n", car.Wheels)
  }

  if car.Structure != "Car" {
    t.Errorf("Structure on a car must be 'Car' and was %s\n", car.Structure)
  }

  if car.Seats != 5 {
    t.Errorf("Seats on a car must be 5 and they were %d\n", car.Seats)
  }

}


我们写了 3 个简单的测试检查是否建造出汽车类型。运行单元测试,结果如下:

$ go test -v .
=== RUN   TestBuilderPattern
    creational_test.go:16: Wheels on a car must be 4 and they were 0
    creational_test.go:20: Structure on a car must be 'Car' and was
    creational_test.go:24: Seats on a car must be 5 and they were 0
--- FAIL: TestBuilderPattern (0.00s)
FAIL
FAIL    github.com/yuzhoustayhungry/GoDesignPattern/creational  0.860s
FAIL

如上显示,3 个测试单元都显示失败,接着我们来看一下摩托车的单元测试怎么写的。



2、摩托车 motorCycle 的单元测试如下:

  motorBuilder := &MotorBuilder{}

  manufacturingComplex.SetBuilder(motorBuilder)
  manufacturingComplex.Construct()

  motorCycle := motorBuilder.GetVehicle()

  if motorCycle.Wheels != 2 {
    t.Errorf("Wheels on a motorCycle must be 2 and they were %d\n",
      motorCycle.Wheels)
  }

  if motorCycle.Structure != "MotorCycle" {
    t.Errorf("Structure on a motorCycle must be 'MotorCycle' and was %s\n",
      motorCycle.Structure)
  }

  if motorCycle.Seats != 2 {
    t.Errorf("Seats on a motorCycle must be 2 and was %d\n", motorCycle.Seats)
  }


建造过程跟 car 类似,我们只需要向 manufacturingComplex.SetBuilder(motorBuilder) 传递 motorBuilder 即可,我们假定摩托车有 2 个轮子,2 个座位,结构必须为 MotorCyle


运行测试代码,得到如下结果:

$ go test -v .
=== RUN   TestBuilderPattern
    creational_test.go:16: Wheels on a car must be 4 and they were 0
    creational_test.go:20: Structure on a car must be 'Car' and was 
    creational_test.go:24: Seats on a car must be 5 and they were 0
    creational_test.go:36: Wheels on a motorCycle must be 2 and they were 0
    creational_test.go:41: Structure on a motorCycle must be 'MotorCycle' and was 
    creational_test.go:46: Seats on a motorCycle must be 2 and was 0
--- FAIL: TestBuilderPattern (0.00s)
FAIL
FAIL    github.com/yuzhoustayhungry/GoDesignPattern/creational  0.595s
FAIL

可以看到,单元测试也是失败的,因为我们还没有完成实现具体的建造者模式。接下来就是具体实现的过程。


3 建造者模式 Go 实现

为了实现建造者,想必你也开始有了一点点自己的思路吧。再来实现我们之前创建的 creation.go 文件,全新的 creational.go 文件代码如下:

package creational

// 建造过程
type BuildProcess interface {
	SetWheels() BuildProcess
	SetSeats() BuildProcess
	SetStructure() BuildProcess
	GetVehicle() VehicleProduct
}

// 制作主管
type ManufacturingDirector struct {
	builder BuildProcess
}

func (f *ManufacturingDirector) Construct() {
	//
	f.builder.SetSeats().SetStructure().SetWheels()
}

func (f *ManufacturingDirector) SetBuilder(b BuildProcess) {
	//
	f.builder = b
}

// 产品
type VehicleProduct struct {
	Wheels    int
	Seats     int
	Structure string
}

// 汽车建造者
type CarBuilder struct {
	v VehicleProduct
}

func (c *CarBuilder) SetWheels() BuildProcess {
	// return nil
	c.v.Wheels = 4
	return c
}

func (c *CarBuilder) SetSeats() BuildProcess {
	// return nil
	c.v.Seats = 5
	return c
}

func (c *CarBuilder) SetStructure() BuildProcess {
	// return nil
	c.v.Structure = "Car"
	return c
}

func (c *CarBuilder) GetVehicle() VehicleProduct {
	// return VehicleProduct{}
	return c.v
}

// 摩托车建造者
type MotorBuilder struct {
	v VehicleProduct
}

func (m *MotorBuilder) SetWheels() BuildProcess {
	// return nil
	m.v.Wheels = 2
	return m
}

func (m *MotorBuilder) SetSeats() BuildProcess {
	// return nil
	m.v.Seats = 2
	return m
}

func (m *MotorBuilder) SetStructure() BuildProcess {
	// return nil
	m.v.Structure = "MotorCycle"
	return m
}

func (m *MotorBuilder) GetVehicle() VehicleProduct {
	// return VehicleProduct{}
	return m.v
}


更改后的 creational_test.go 文件如下:

package creational

import "testing"

func TestBuilderPattern(t *testing.T) {

	manufacturingComplex := ManufacturingDirector{}

	carBuilder := &CarBuilder{}
	manufacturingComplex.SetBuilder(carBuilder)
	manufacturingComplex.Construct()

	car := carBuilder.GetVehicle()

	if car.Wheels != 4 {
		t.Errorf("Wheels on a car must be 4 and they were %d\n", car.Wheels)
	}

	if car.Structure != "Car" {
		t.Errorf("Structure on a car must be 'Car' and was %s\n", car.Structure)
	}

	if car.Seats != 5 {
		t.Errorf("Seats on a car must be 5 and they were %d\n", car.Seats)
	}

	motorBuilder := &MotorBuilder{}

	manufacturingComplex.SetBuilder(motorBuilder)
	manufacturingComplex.Construct()

	motorCycle := motorBuilder.GetVehicle()

	if motorCycle.Wheels != 2 {
		t.Errorf("Wheels on a motorCycle must be 2 and they were %d\n",
			motorCycle.Wheels)
	}

	if motorCycle.Structure != "MotorCycle" {
		t.Errorf("Structure on a motorCycle must be 'MotorCycle' and was %s\n",
			motorCycle.Structure)
	}

	if motorCycle.Seats != 2 {
		t.Errorf("Seats on a motorCycle must be 2 and was %d\n", motorCycle.Seats)
	}

}


实现完所有的方法之后,再看运行 go test -v . 执行后的测试结果:

$ go test -v .
=== RUN   TestBuilderPattern
--- PASS: TestBuilderPattern (0.00s)
PASS
ok      github.com/yuzhoustayhungry/GoDesignPattern/creational  0.255s

恭喜,至此,测试用例全部通过。你也可以看到,建造者模式是一个可重复的模式,但在 BuildProcess 接口的每个方法内,我们可以封装尽可能多的复杂对象,这样,用户其实并不知道关于对象创建的细节。


4 建造者模式总结

就像制作汽车一样,建造者模式的核心在于如何一步一步地构建一个包含多个组成部件的完整对象,使用相同的构建过程构建不同的产品。


在软件开发过程中,如果需要创建复杂对象,并希望系统具备很好的灵活性和可扩展性,可以考虑使用建造者模式。

希望本文能对你有所帮助,如果喜欢本文,可以点个关注。

这里是宇宙之一粟,下一篇文章见!

宇宙古今无有穷期,一生不过须臾,当思奋争。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。