经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » 设计模式 » 查看文章
浅析建造者模式
来源:cnblogs  作者:lubanseven  时间:2023/7/17 9:38:52  对本文有异议

0. 前言

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

1. 建造者模式

1.1 从工厂函数说起

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

  1. type house struct {
  2. window int
  3. door int
  4. bed int
  5. desk int
  6. deskLamp int
  7. }

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

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

  1. func NewHouse(window, door, bed, desk, deskLamp int) *house {
  2. return &house{
  3. window: window,
  4. door: door,
  5. bed: bed,
  6. desk: desk,
  7. deskLamp: deskLamp,
  8. }
  9. }

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

  1. house := NewHouse(2, 1, 1, 0, 0)

这对调用者来说不必要。

继续,使用 set 结合工厂函数构造 house 对象:

  1. func NewHouse(window, door, bed int) *house {
  2. return &house{
  3. window: window,
  4. door: door,
  5. bed: bed
  6. }
  7. }
  8. func (h *house) SetDesk(desk int) {
  9. h.desk = desk
  10. }
  11. func (h *house) SetDeskLamp(deskLamp int) {
  12. h.deskLamp = deskLamp
  13. }

创建 house 对象:

  1. house := NewHouse(2, 1, 1)
  2. // 使用 set 设置 desk 和 deskLamp
  3. house.SetDesk(1)
  4. house.SetDeskLamp(1)

看起来还不错。

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

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

这两种思路虽然能实现校验功能,但是都有瑕疵。
思路一,在构造完对象后才验证,如果对象忘了调用 validation,那这个对象就是个不安全的对象。
思路二,校验分开了,对象的属性应该放在一起校验,试想如果参数过多,且相互有依赖关系,那又得新增方法判断,麻烦且容易出错。

并且,对于调用方来说,构造过程暴露太多了。工厂函数的优势在于调用方无感知,如果暴露太多 set 方法,并且由调用方来调用验证方法验证对象属性。那工厂函数的优势将大打折扣。

1.2 工厂函数到建造者的优雅过渡

如何适配上述场景,使得调用方无感知呢?

试拆分上述代码如下:

  1. func NewHouse() *house {
  2. return &house{}
  3. }
  4. func (h *house) SetRequisite(window, door, bed int) *house {
  5. h.window = window
  6. h.door = door
  7. h.bed = bed
  8. return h
  9. }
  10. func (h *house) SetDesk(desk int) *house {
  11. h.desk = desk
  12. return h
  13. }
  14. func (h *house) SetDeskLamp(deskLamp int) *house {
  15. h.deskLamp = deskLamp
  16. return h
  17. }
  18. func (h *house) Validation() (*house, error) {
  19. if h.window <= 0 || h.door <= 0 || h.bed <= 0 {
  20. return nil, errors.New("invalid [window|door|bed]")
  21. }
  22. if h.desk < 0 || h.deskLamp < 0 {
  23. return nil, errors.New("invalid [desk|deskLamp]")
  24. }
  25. if !(h.desk > 0 && h.deskLamp > 0) {
  26. return nil, errors.New("need desk and deskLamp at same time")
  27. }
  28. return h, nil
  29. }

创建 house 对象:

  1. house, _ := NewHouse().SetRequisite(2, 1, 1).SetDesk(1).SetDeskLamp(1).Validation()

嗯,看起来清晰了不少。不过我们细细分析下逻辑的话还是会发现那么一点怪异的点。这一点在于,house 对象是 set 的主体,这在逻辑上好像不通。

是的,我们需要引入一个新对象叫 Builder 来创建 house,而不是让 house 自己创建自己。

改造代码如下:
示例 1.1

  1. type Builder struct {
  2. house
  3. }
  4. func NewBuilder() *Builder {
  5. return &Builder{}
  6. }
  7. func (b *Builder) SetRequisite(window, door, bed int) *Builder {
  8. b.window = window
  9. b.door = door
  10. b.bed = bed
  11. return b
  12. }
  13. func (b *Builder) SetDesk(desk int) *Builder {
  14. b.desk = desk
  15. return b
  16. }
  17. func (b *Builder) SetDeskLamp(deskLamp int) *Builder {
  18. b.deskLamp = deskLamp
  19. return b
  20. }
  21. func (b *Builder) build() (*house, error) {
  22. if b.window <= 0 || b.door <= 0 || b.bed <= 0 {
  23. return nil, errors.New("invalid [window|door|bed]")
  24. }
  25. if b.desk < 0 || b.deskLamp < 0 {
  26. return nil, errors.New("invalid [desk|deskLamp]")
  27. }
  28. if !(b.desk > 0 && b.deskLamp > 0) {
  29. return nil, errors.New("need desk and deskLamp at same time")
  30. }
  31. return &b.house, nil
  32. }

这里做了几点改动:
1)新建 Builder 对象,通过 Builder 对象创建 house。并且,将 house 作为 Builder 的属性,houseBuilder 造的,作为属性挺合理的。
2)重命名 Validationbuild,之所以这么命名是想说明 build 是创建的最后一步,结束 build 之后即可获得 house 对象。

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

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

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

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

要留意这种结构,它是顺序不一致的。
如果顺序一致的情况,即创建的流程都是一样的。那么可以将 build 抽象为接口,使用不同的接口创建产品,且创建的产品流程是一样的,可以用封装将这一过程封装起来。

举例,使用两个 Builder 创建房子。villaBuilder 先建十个门,再建五十个窗,最后放五十把椅子。residenceBuilder 负责建两个门,两个窗,以及五把椅子。代码如下:

  1. type Builder interface {
  2. createDoor() Builder
  3. createWindow() Builder
  4. createChair() Builder
  5. build() (*house, error)
  6. }
  7. type villaBuilder struct {
  8. house
  9. }
  10. type residenceBuilder struct {
  11. house
  12. }
  13. type house struct {
  14. door int
  15. window int
  16. chair int
  17. }
  18. func (vb *villaBuilder) createDoor() Builder {
  19. vb.door = 10
  20. return vb
  21. }
  22. func (vb *villaBuilder) createWindow() Builder {
  23. vb.window = 50
  24. return vb
  25. }
  26. func (vb *villaBuilder) createChair() Builder {
  27. vb.chair = 50
  28. return vb
  29. }
  30. func (vb *villaBuilder) validation() error {
  31. return nil
  32. }
  33. func (vb *villaBuilder) build() (*house, error) {
  34. // validate property of object houseBuilder, skip...
  35. err := vb.validation()
  36. vb.createDoor()
  37. vb.createWindow()
  38. vb.createChair()
  39. return &vb.house, err
  40. }
  41. func (rb *residenceBuilder) createDoor() Builder {
  42. rb.door = 2
  43. return rb
  44. }
  45. func (rb *residenceBuilder) createWindow() Builder {
  46. rb.window = 2
  47. return rb
  48. }
  49. func (rb *residenceBuilder) createChair() Builder {
  50. rb.chair = 1
  51. return rb
  52. }
  53. func (rb *residenceBuilder) validation() error {
  54. return nil
  55. }
  56. func (rb *residenceBuilder) build() (*house, error) {
  57. // validate property of object carBuilder, skip...
  58. err := rb.validation()
  59. rb.createDoor()
  60. rb.createWindow()
  61. rb.createChair()
  62. return &rb.house, err
  63. }
  64. func NewBuilder(typ string) Builder {
  65. switch typ {
  66. case "villa":
  67. return &villaBuilder{}
  68. case "residence":
  69. return &residenceBuilder{}
  70. default:
  71. return nil
  72. }
  73. }

最后,通过不同类型的 Builder 创建房子:

  1. house, err := NewBuilder("villa").build()

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

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

重新改造代码如下:

  1. type villaBuilder struct {
  2. house
  3. window int
  4. door int
  5. chair int
  6. }
  7. func (hb *villaBuilder) createDoor(door int) Builder {
  8. hb.house.door = door
  9. return hb
  10. }
  11. func (hb *villaBuilder) createWindow(window int) Builder {
  12. hb.house.window = window
  13. return hb
  14. }
  15. func (hb *villaBuilder) createChair(chair int) Builder {
  16. hb.house.chair = chair
  17. return hb
  18. }
  19. func (hb *villaBuilder) build() (*house, error) {
  20. // validate property of object villaBuilder, skip...
  21. err := hb.validate()
  22. hb.createDoor(hb.door)
  23. hb.createWindwo(hb.window)
  24. hb.createChair(hb.chair)
  25. return hb.car, err
  26. }
  27. func NewBuilder(typ string) Builder {
  28. switch typ {
  29. case "villa":
  30. return &villaBuilder{}
  31. case "residence":
  32. return &residenceBuilder{}
  33. default:
  34. return nil
  35. }
  36. }

调用方创建 house

  1. house, err := NewBuilder("house", 2, 2, 2).build()

这里最大的改变在于 Builder 对象中新增可配置属性 window, doorchair。通过 Builer 内的属性将参数传给内嵌产品对象,实现有序创建。

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

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

同样的道理,将可选项赋值给 Builder 中的属性。代码如下:
示例 1.2

  1. type villaBuilder struct {
  2. house
  3. window int
  4. door int
  5. chair int
  6. icer int
  7. drink int
  8. }
  9. func (vb *villaBuilder) createIcer() Builder {
  10. vb.house.icer = vb.icer
  11. return vb
  12. }
  13. func (vb *villaBuilder) createDrink() Builder {
  14. vb.house.drink = vb.drink
  15. return vb
  16. }
  17. func (vb *villaBuilder) setIcer(icer int) Builder {
  18. vb.icer = icer
  19. return vb
  20. }
  21. func (hb villaBuilder) setDrink(drink int) Builder {
  22. vb.drink = drink
  23. return vb
  24. }

调用方创建过程为:

  1. house, err := NewBuilder("house", 2, 2, 2).setIcer(1).setDrink(1).build()

调用方一直在和 Builder 打交道。可选配置传递给 Builder,最后通过 build 创建出 house,做到了表达和实现分离。

1.3 建造者模式

讲到这里基本也差不多了,在建造者模式中还有个 Director 对象作为更上层的封装。

从上面代码示例中,Builder 负责整体的顺序创建,可以把这块逻辑向上提给 DirectorBuilder 只关心部件的创建,而不需要关心整体。做到逻辑的进一步拆分。代码示例如下:

  1. type Director struct {
  2. builder Builder
  3. }
  4. func (d *Director) createHouse() (*house, error) {
  5. if err := d.builder.validation(); err != nil {
  6. return nil, err
  7. }
  8. d.builder.createDoor()
  9. d.builder.createWindwo()
  10. d.builder.createChair()
  11. return *hb.house, nil
  12. }

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

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

1.4 建造者模式在 Kubernetes:kubectl 的应用

kubectl 上找到了建造者模式的应用,虽然不是“完全体”,不过没有关系。代码如下:

  1. // https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/get/get.go
  2. r := f.NewBuilder().
  3. Unstructured().
  4. NamespaceParam(o.Namespace).DefaultNamespace().AllNamespaces(o.AllNamespaces).
  5. FilenameParam(o.ExplicitNamespace, &o.FilenameOptions).
  6. LabelSelectorParam(o.LabelSelector).
  7. FieldSelectorParam(o.FieldSelector).
  8. Subresource(o.Subresource).
  9. RequestChunksOf(chunkSize).
  10. ResourceTypeOrNameArgs(true, args...).
  11. ContinueOnError().
  12. Latest().
  13. Flatten().
  14. TransformRequests(o.transformRequests).
  15. Do()

这段代码是不是和我们的示例 1.2 非常像。通过 factoryNewBuilder 创建 Builder,接着通过一系列建造者方法构造 Builder,最后构建完成的 Builder 调用 Do 方法创建 resouce.Result 对象。

2. 小结

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

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

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


原文链接:https://www.cnblogs.com/xingzheanan/p/17558133.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号