代码组件 | Go设计模式实战

嗯,Go设计模式实战系列,一个设计模式业务真实使用的golang系列。

http://tigerb.cn/

前言

本系列主要分享,如何在我们的真实业务场景中使用设计模式。

本系列文章主要采用如下结构:

  • 什么是「XX设计模式」?
  • 什么真实业务场景可以使用「XX设计模式」?
  • 怎么用「XX设计模式」?

本文主要介绍「组合模式」如何在真实业务场景中使用。

什么是「组合模式」?

一个具有层级关系的对象由一系列拥有父子关系的对象通过树形结构组成。

组合模式的优势:

  • 所见即所码:你所看见的代码结构就是业务真实的层级关系,比如Ui界面你真实看到的那样。
  • 高度封装:单一职责。
  • 可复用:不同业务场景,相同的组件可被重复使用。

什么真实业务场景可以用「组合模式」?

满足如下要求的所有场景:

Get请求获取页面数据的所有接口

前端大行组件化的当今,我们在写后端接口代码的时候还是按照业务思路一头写到尾吗?我们是否可以思索,「后端接口业务代码如何可以简单快速组件化?」,答案是肯定的,这就是「组合模式」的作用。

我们利用「组合模式」的定义和前端模块的划分去构建后端业务代码结构:

  • 前端单个模块 -> 对应后端:具体单个类 -> 封装的过程
  • 前端模块父子组件 -> 对应后端:父类内部持有多个子类(非继承关系,合成复用关系) -> 父子关系的树形结构

我们有哪些真实业务场景可以用「组合模式」呢?

比如我们以“复杂的订单结算页面”为例,下面是某东的订单结算页面:

从页面的展示形式上,可以看出:

  • 页面由多个模块构成,比如:
    • 地址模块
    • 支付方式模块
    • 店铺模块
    • 发票模块
    • 优惠券模块
    • 某豆模块
    • 礼品卡模块
    • 订单详细金额模块
  • 单个模块可以由多个子模块构成
    • 店铺模块,又由如下模块构成:
      • 商品模块
      • 售后模块
      • 优惠模块
      • 物流模块

怎么用「组合模式」?

关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:

  • 业务梳理
  • 业务流程图
  • 代码建模
  • 代码demo

业务梳理

按照如上某东的订单结算页面的示例,我们得到了如下的订单结算页面模块组成图:

注:模块不一定完全准确

代码建模

责任链模式主要类主要包含如下特性:

  • 成员属性
    • ChildComponents: 子组件列表 -> 稳定不变的
  • 成员方法
    • Mount: 添加一个子组件 -> 稳定不变的
    • Remove: 移除一个子组件 -> 稳定不变的
    • Do: 执行组件&子组件 -> 变化的

套用到订单结算页面信息接口伪代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
一个父类(抽象类):
- 成员属性
+ `ChildComponents`: 子组件列表
- 成员方法
+ `Mount`: 实现添加一个子组件
+ `Remove`: 实现移除一个子组件
+ `Do`: 抽象方法

组件一,订单结算页面组件类(继承父类、看成一个大的组件):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件二,地址组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件三,支付方式组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件四,店铺组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件五,商品组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件六,优惠信息组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件七,物流组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件八,发票组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件九,优惠券组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件十,礼品卡组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件十一,订单金额详细信息组件(继承父类):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑
组件十二,售后组件(继承父类,未来扩展的组件):
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

但是,golang里没有的继承的概念,要复用成员属性ChildComponents、成员方法Mount、成员方法Remove怎么办呢?我们使用合成复用的特性变相达到“继承复用”的目的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
一个接口(interface):
+ 抽象方法`Mount`: 添加一个子组件
+ 抽象方法`Remove`: 移除一个子组件
+ 抽象方法`Do`: 执行组件&子组件

一个基础结构体`BaseComponent`:
- 成员属性
+ `ChildComponents`: 子组件列表
- 成员方法
+ 实体方法`Mount`: 添加一个子组件
+ 实体方法`Remove`: 移除一个子组件
+ 实体方法`ChildsDo`: 执行子组件

组件一,订单结算页面组件类:
- 合成复用基础结构体`BaseComponent`
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件二,地址组件:
- 合成复用基础结构体`BaseComponent`
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

组件三,支付方式组件:
- 合成复用基础结构体`BaseComponent`
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

...略

组件十一,订单金额详细信息组件:
- 合成复用基础结构体`BaseComponent`
- 成员方法
+ `Do`: 执行当前组件的逻辑,执行子组件的逻辑

同时得到了我们的UML图:

代码demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package main

import (
"fmt"
"reflect"
"runtime"
)

//------------------------------------------------------------
//我的代码没有`else`系列
//组合模式
//@auhtor TIGERB<https://github.com/TIGERB>
//------------------------------------------------------------

// Context 上下文
type Context struct{}

// Component 组件接口
type Component interface {
// 添加一个子组件
Mount(c Component, components ...Component) error
// 移除一个子组件
Remove(c Component) error
// 执行组件&子组件
Do(ctx *Context) error
}

// BaseComponent 基础组件
// 实现Add:添加一个子组件
// 实现Remove:移除一个子组件
type BaseComponent struct {
// 子组件列表
ChildComponents []Component
}

// Mount 挂载一个子组件
func (bc *BaseComponent) Mount(c Component, components ...Component) (err error) {
bc.ChildComponents = append(bc.ChildComponents, c)
if len(components) == 0 {
return
}
bc.ChildComponents = append(bc.ChildComponents, components...)
return
}

// Remove 移除一个子组件
func (bc *BaseComponent) Remove(c Component) (err error) {
if len(bc.ChildComponents) == 0 {
return
}
for k, childComponent := range bc.ChildComponents {
if c == childComponent {
fmt.Println(runFuncName(), "移除:", reflect.TypeOf(childComponent))
bc.ChildComponents = append(bc.ChildComponents[:k], bc.ChildComponents[k+1:]...)
}
}
return
}

// Do 执行组件&子组件
func (bc *BaseComponent) Do(ctx *Context) (err error) {
// do nothing
return
}

// ChildsDo 执行子组件
func (bc *BaseComponent) ChildsDo(ctx *Context) (err error) {
// 执行子组件
for _, childComponent := range bc.ChildComponents {
if err = childComponent.Do(ctx); err != nil {
return err
}
}
return
}

// CheckoutPageComponent 订单结算页面组件
type CheckoutPageComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *CheckoutPageComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "订单结算页面组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// AddressComponent 地址组件
type AddressComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *AddressComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "地址组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// PayMethodComponent 支付方式组件
type PayMethodComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *PayMethodComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "支付方式组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// StoreComponent 店铺组件
type StoreComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *StoreComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "店铺组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// SkuComponent 商品组件
type SkuComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *SkuComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "商品组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// PromotionComponent 优惠信息组件
type PromotionComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *PromotionComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "优惠信息组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// ExpressComponent 物流组件
type ExpressComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *ExpressComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "物流组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// AftersaleComponent 售后组件
type AftersaleComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *AftersaleComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "售后组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// InvoiceComponent 发票组件
type InvoiceComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *InvoiceComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "发票组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// CouponComponent 优惠券组件
type CouponComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *CouponComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "优惠券组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// GiftCardComponent 礼品卡组件
type GiftCardComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *GiftCardComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "礼品卡组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

// OrderComponent 订单金额详细信息组件
type OrderComponent struct {
// 合成复用基础组件
BaseComponent
}

// Do 执行组件&子组件
func (bc *OrderComponent) Do(ctx *Context) (err error) {
// 当前组件的业务逻辑写这
fmt.Println(runFuncName(), "订单金额详细信息组件...")

// 执行子组件
bc.ChildsDo(ctx)

// 当前组件的业务逻辑写这

return
}

func main() {
// 初始化订单结算页面 这个大组件
checkoutPage := &CheckoutPageComponent{}

// 挂载子组件
storeComponent := &StoreComponent{}
skuComponent := &SkuComponent{}
skuComponent.Mount(
&PromotionComponent{},
&AftersaleComponent{},
)
storeComponent.Mount(
skuComponent,
&ExpressComponent{},
)

// 挂载组件
checkoutPage.Mount(
&AddressComponent{},
&PayMethodComponent{},
storeComponent,
&InvoiceComponent{},
&CouponComponent{},
&GiftCardComponent{},
&OrderComponent{},
)

// 移除组件测试
// checkoutPage.Remove(storeComponent)

// 开始构建页面组件数据
checkoutPage.Do(&Context{})
}

// 获取正在运行的函数名
func runFuncName() string {
pc := make([]uintptr, 1)
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return f.Name()
}


代码运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Running] go run "../easy-tips/go/src/patterns/composite/composite.go"
main.(*CheckoutPageComponent).Do 订单结算页面组件...
main.(*AddressComponent).Do 地址组件...
main.(*PayMethodComponent).Do 支付方式组件...
main.(*StoreComponent).Do 店铺组件...
main.(*SkuComponent).Do 商品组件...
main.(*PromotionComponent).Do 优惠信息组件...
main.(*AftersaleComponent).Do 售后组件...
main.(*ExpressComponent).Do 物流组件...
main.(*InvoiceComponent).Do 发票组件...
main.(*CouponComponent).Do 优惠券组件...
main.(*GiftCardComponent).Do 礼品卡组件...
main.(*OrderComponent).Do 订单金额详细信息组件...

结语

最后总结下,「组合模式」抽象过程的核心是:

  • 按模块划分:业务逻辑归类,收敛的过程。
  • 父子关系(树):把收敛之后的业务对象按父子关系绑定,依次被执行。

与「责任链模式」的区别:

  • 责任链模式: 链表
  • 组合模式:树
1
2
3
特别说明:
1. 我的代码没有`else`,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。
2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。

文章列表

Go设计模式实战系列 更多文章 点击此处查看