前言
[作者简介] 施展,小米信息技术部海外商城组
本文章主要采用如下结构:
- 什么是「XX 设计模式」?
- 什么真实业务场景可以使用「XX 设计模式」?
- 怎么用「XX 设计模式」?
本文主要介绍「模板模式」如何在真实业务场景中使用。
什么是「模板模式」?
抽象类里定义好算法的执行步骤和具体算法,以及可能发生变化的算法定义为抽象方法。不同的子类继承该抽象类,并实现父类的抽象方法。
模板模式的优势:
- 不变的算法被继承复用:不变的部分高度封装、复用。
- 变化的算法子类继承并具体实现:变化的部分子类只需要具体实现抽象的部分即可,方便扩展,且可无限扩展。
什么真实业务场景可以用「模板模式」?
满足如下要求的所有场景:
算法执行的步骤是稳定不变的,但是具体的某些算法可能存在变化的场景。
怎么理解,举个例子:比如说你煮个面,必然需要先烧水,水烧开之后再放面进去
,以上的流程我们称之为煮面过程
。可知:这个煮面过程
的步骤是稳定不变的,但是在不同的环境烧水的方式可能不尽相同,也许有的人用天然气烧水、有的人用电磁炉烧水、有的人用柴火烧水,等等。我们可以得到以下结论:
煮面过程
的步骤是稳定不变的
煮面过程
的烧水方式是可变的
我们有哪些真实业务场景可以用「模板模式」呢?
比如抽奖系统的抽奖接口,为什么:
- 抽奖的步骤是稳定不变的 -> 不变的算法执行步骤
- 不同抽奖类型活动在某些逻辑处理方式可能不同 -> 变的某些算法
怎么用「模板模式」?
关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:
业务梳理
我通过历史上接触过的各种抽奖场景(红包雨、糖果雨、打地鼠、大转盘(九宫格)、考眼力、答题闯关、游戏闯关、支付刮刮乐、积分刮刮乐等等),按照真实业务需求梳理了以下抽奖业务抽奖接口的大致文本流程。
主步骤 |
主逻辑 |
抽奖类型 |
子步骤 |
子逻辑 |
1 |
校验活动编号 (serial_no) 是否存在、并获取活动信息 |
- |
- |
- |
2 |
校验活动、场次是否正在进行 |
- |
- |
- |
3 |
其他参数校验 (不同活动类型实现不同) |
- |
- |
- |
4 |
活动抽奖次数校验(同时扣减) |
- |
- |
- |
5 |
活动是否需要消费积分 |
- |
- |
- |
6 |
场次抽奖次数校验(同时扣减) |
- |
- |
- |
7 |
获取场次奖品信息 |
- |
- |
- |
8 |
获取 node 奖品信息 (不同活动类型实现不同) |
按时间抽奖类型 |
1 |
do nothing(抽取该场次的奖品即可,无需其他逻辑) |
8 |
|
按抽奖次数抽奖类型 |
1 |
判断是该用户第几次抽奖 |
8 |
|
|
2 |
获取对应 node 的奖品信息 |
8 |
|
|
3 |
复写原所有奖品信息(抽取该 node 节点的奖品) |
8 |
|
按数额范围区间抽奖 |
1 |
判断属于哪个数额区间 |
8 |
|
|
2 |
获取对应 node 的奖品信息 |
8 |
|
|
3 |
复写原所有奖品信息(抽取该 node 节点的奖品) |
9 |
抽奖 |
- |
- |
- |
10 |
奖品数量判断 |
- |
- |
- |
11 |
组装奖品信息 |
- |
- |
- |
注:流程不一定完全准确
结论:
主逻辑
是稳定不变的
其他参数校验
和获取 node 奖品信息
的算法是可变的
业务流程图
我们通过梳理的文本业务流程得到了如下的业务流程图:
代码建模
通过上面的分析我们可以得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 一个抽象类 - 具体共有方法`Run`,里面定义了算法的执行步骤 - 具体私有方法,不会发生变化的具体方法 - 抽象方法,会发生变化的方法
子类一(按时间抽奖类型) - 继承抽象类父类 - 实现抽象方法
子类二(按抽奖次数抽奖类型) - 继承抽象类父类 - 实现抽象方法
子类三(按数额范围区间抽奖) - 继承抽象类父类 - 实现抽象方法
|
但是 golang 里面没有继承的概念,我们就把对抽象类里抽象方法的依赖转化成对接口interface
里抽想方法的依赖,同时也可以利用合成复用
的方式“继承”模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 抽象行为的接口`BehaviorInterface`(包含如下需要实现的方法) - 其他参数校验的方法`checkParams` - 获取 node 奖品信息的方法`getPrizesByNode`
抽奖结构体类 - 具体共有方法`Run`,里面定义了算法的执行步骤 - 具体私有方法`checkParams` 里面的逻辑实际依赖的接口 BehaviorInterface.checkParams(ctx) 的抽象方法 - 具体私有方法`getPrizesByNode` 里面的逻辑实际依赖的接口 BehaviorInterface.getPrizesByNode(ctx) 的抽象方法 - 其他具体私有方法,不会发生变化的具体方法
实现`BehaviorInterface`的结构体一(按时间抽奖类型) - 实现接口方法
实现`BehaviorInterface`的结构体二(按抽奖次数抽奖类型) - 实现接口方法
实现`BehaviorInterface`的结构体三(按数额范围区间抽奖) - 实现接口方法
|
同时得到了我们的 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
| package main
import ( "fmt" "runtime" )
const ( ConstActTypeTime int32 = 1 ConstActTypeTimes int32 = 2 ConstActTypeAmount int32 = 3 )
type Context struct { ActInfo *ActInfo }
type ActInfo struct { ActivityType int32 }
type BehaviorInterface interface { checkParams(ctx *Context) error getPrizesByNode(ctx *Context) error }
type TimeDraw struct{}
func (draw TimeDraw) checkParams(ctx *Context) (err error) { fmt.Println(runFuncName(), "按时间抽奖类型:特殊参数校验。..") return }
func (draw TimeDraw) getPrizesByNode(ctx *Context) (err error) { fmt.Println(runFuncName(), "do nothing(抽取该场次的奖品即可,无需其他逻辑)...") return }
type TimesDraw struct{}
func (draw TimesDraw) checkParams(ctx *Context) (err error) { fmt.Println(runFuncName(), "按抽奖次数抽奖类型:特殊参数校验。..") return }
func (draw TimesDraw) getPrizesByNode(ctx *Context) (err error) { fmt.Println(runFuncName(), "1. 判断是该用户第几次抽奖。..") fmt.Println(runFuncName(), "2. 获取对应 node 的奖品信息。..") fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该 node 节点的奖品)...") return }
type AmountDraw struct{}
func (draw *AmountDraw) checkParams(ctx *Context) (err error) { fmt.Println(runFuncName(), "按数额范围区间抽奖:特殊参数校验。..") return }
func (draw *AmountDraw) getPrizesByNode(ctx *Context) (err error) { fmt.Println(runFuncName(), "1. 判断属于哪个数额区间。..") fmt.Println(runFuncName(), "2. 获取对应 node 的奖品信息。..") fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该 node 节点的奖品)...") return }
type Lottery struct { concreteBehavior BehaviorInterface }
func (lottery *Lottery) Run(ctx *Context) (err error) { if err = lottery.checkSerialNo(ctx); err != nil { return err }
if err = lottery.checkStatus(ctx); err != nil { return err }
if err = lottery.checkParams(ctx); err != nil { return err }
if err = lottery.checkTimesByAct(ctx); err != nil { return err }
if err = lottery.consumePointsByAct(ctx); err != nil { return err }
if err = lottery.checkTimesBySession(ctx); err != nil { return err }
if err = lottery.getPrizesBySession(ctx); err != nil { return err }
if err = lottery.getPrizesByNode(ctx); err != nil { return err }
if err = lottery.drawPrizes(ctx); err != nil { return err }
if err = lottery.checkPrizesStock(ctx); err != nil { return err }
if err = lottery.packagePrizeInfo(ctx); err != nil { return err } return }
func (lottery *Lottery) checkSerialNo(ctx *Context) (err error) { fmt.Println(runFuncName(), "校验活动编号 (serial_no) 是否存在、并获取活动信息。..") ctx.ActInfo = &ActInfo{ ActivityType: ConstActTypeTimes, }
switch ctx.ActInfo.ActivityType { case 1: lottery.concreteBehavior = &TimeDraw{} case 2: lottery.concreteBehavior = &TimesDraw{} case 3: lottery.concreteBehavior = &AmountDraw{} default: return fmt.Errorf("不存在的活动类型") } return }
func (lottery *Lottery) checkStatus(ctx *Context) (err error) { fmt.Println(runFuncName(), "校验活动、场次是否正在进行。..") return }
func (lottery *Lottery) checkParams(ctx *Context) (err error) { return lottery.concreteBehavior.checkParams(ctx) }
func (lottery *Lottery) checkTimesByAct(ctx *Context) (err error) { fmt.Println(runFuncName(), "活动抽奖次数校验。..") return }
func (lottery *Lottery) consumePointsByAct(ctx *Context) (err error) { fmt.Println(runFuncName(), "活动是否需要消费积分。..") return }
func (lottery *Lottery) checkTimesBySession(ctx *Context) (err error) { fmt.Println(runFuncName(), "活动抽奖次数校验。..") return }
func (lottery *Lottery) getPrizesBySession(ctx *Context) (err error) { fmt.Println(runFuncName(), "获取场次奖品信息。..") return }
func (lottery *Lottery) getPrizesByNode(ctx *Context) (err error) { return lottery.concreteBehavior.getPrizesByNode(ctx) }
func (lottery *Lottery) drawPrizes(ctx *Context) (err error) { fmt.Println(runFuncName(), "抽奖。..") return }
func (lottery *Lottery) checkPrizesStock(ctx *Context) (err error) { fmt.Println(runFuncName(), "奖品数量判断。..") return }
func (lottery *Lottery) packagePrizeInfo(ctx *Context) (err error) { fmt.Println(runFuncName(), "组装奖品信息。..") return }
func main() { (&Lottery{}).Run(&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 14
| [Running] go run ".../easy-tips/go/src/patterns/template/template.go" main.(*Lottery).checkSerialNo 校验活动编号 (serial_no) 是否存在、并获取活动信息。.. main.(*Lottery).checkStatus 校验活动、场次是否正在进行。.. main.TimesDraw.checkParams 按抽奖次数抽奖类型:特殊参数校验。.. main.(*Lottery).checkTimesByAct 活动抽奖次数校验。.. main.(*Lottery).consumePointsByAct 活动是否需要消费积分。.. main.(*Lottery).checkTimesBySession 活动抽奖次数校验。.. main.(*Lottery).getPrizesBySession 获取场次奖品信息。.. main.TimesDraw.getPrizesByNode 1. 判断是该用户第几次抽奖。.. main.TimesDraw.getPrizesByNode 2. 获取对应 node 的奖品信息。.. main.TimesDraw.getPrizesByNode 3. 复写原所有奖品信息(抽取该 node 节点的奖品)... main.(*Lottery).drawPrizes 抽奖。.. main.(*Lottery).checkPrizesStock 奖品数量判断。.. main.(*Lottery).packagePrizeInfo 组装奖品信息。..
|
demo 代码地址:https://github.com/TIGERB/easy-tips/blob/master/go/src/patterns/template/template.go
代码 demo2(利用 golang 的合成复用
特性实现)
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
| package main
import ( "fmt" "runtime" )
const ( ConstActTypeTime int32 = 1 ConstActTypeTimes int32 = 2 ConstActTypeAmount int32 = 3 )
type Context struct { ActInfo *ActInfo }
type ActInfo struct { ActivityType int32 }
type BehaviorInterface interface { checkParams(ctx *Context) error getPrizesByNode(ctx *Context) error }
type TimeDraw struct { Lottery }
func (draw TimeDraw) checkParams(ctx *Context) (err error) { fmt.Println(runFuncName(), "按时间抽奖类型:特殊参数校验。..") return }
func (draw TimeDraw) getPrizesByNode(ctx *Context) (err error) { fmt.Println(runFuncName(), "do nothing(抽取该场次的奖品即可,无需其他逻辑)...") return }
type TimesDraw struct { Lottery }
func (draw TimesDraw) checkParams(ctx *Context) (err error) { fmt.Println(runFuncName(), "按抽奖次数抽奖类型:特殊参数校验。..") return }
func (draw TimesDraw) getPrizesByNode(ctx *Context) (err error) { fmt.Println(runFuncName(), "1. 判断是该用户第几次抽奖。..") fmt.Println(runFuncName(), "2. 获取对应 node 的奖品信息。..") fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该 node 节点的奖品)...") return }
type AmountDraw struct { Lottery }
func (draw *AmountDraw) checkParams(ctx *Context) (err error) { fmt.Println(runFuncName(), "按数额范围区间抽奖:特殊参数校验。..") return }
func (draw *AmountDraw) getPrizesByNode(ctx *Context) (err error) { fmt.Println(runFuncName(), "1. 判断属于哪个数额区间。..") fmt.Println(runFuncName(), "2. 获取对应 node 的奖品信息。..") fmt.Println(runFuncName(), "3. 复写原所有奖品信息(抽取该 node 节点的奖品)...") return }
type Lottery struct { ConcreteBehavior BehaviorInterface }
func (lottery *Lottery) Run(ctx *Context) (err error) { if err = lottery.checkSerialNo(ctx); err != nil { return err }
if err = lottery.checkStatus(ctx); err != nil { return err }
if err = lottery.checkParams(ctx); err != nil { return err }
if err = lottery.checkTimesByAct(ctx); err != nil { return err }
if err = lottery.consumePointsByAct(ctx); err != nil { return err }
if err = lottery.checkTimesBySession(ctx); err != nil { return err }
if err = lottery.getPrizesBySession(ctx); err != nil { return err }
if err = lottery.getPrizesByNode(ctx); err != nil { return err }
if err = lottery.drawPrizes(ctx); err != nil { return err }
if err = lottery.checkPrizesStock(ctx); err != nil { return err }
if err = lottery.packagePrizeInfo(ctx); err != nil { return err } return }
func (lottery *Lottery) checkSerialNo(ctx *Context) (err error) { fmt.Println(runFuncName(), "校验活动编号 (serial_no) 是否存在、并获取活动信息。..") return }
func (lottery *Lottery) checkStatus(ctx *Context) (err error) { fmt.Println(runFuncName(), "校验活动、场次是否正在进行。..") return }
func (lottery *Lottery) checkParams(ctx *Context) (err error) { return lottery.ConcreteBehavior.checkParams(ctx) }
func (lottery *Lottery) checkTimesByAct(ctx *Context) (err error) { fmt.Println(runFuncName(), "活动抽奖次数校验。..") return }
func (lottery *Lottery) consumePointsByAct(ctx *Context) (err error) { fmt.Println(runFuncName(), "活动是否需要消费积分。..") return }
func (lottery *Lottery) checkTimesBySession(ctx *Context) (err error) { fmt.Println(runFuncName(), "活动抽奖次数校验。..") return }
func (lottery *Lottery) getPrizesBySession(ctx *Context) (err error) { fmt.Println(runFuncName(), "获取场次奖品信息。..") return }
func (lottery *Lottery) getPrizesByNode(ctx *Context) (err error) { return lottery.ConcreteBehavior.getPrizesByNode(ctx) }
func (lottery *Lottery) drawPrizes(ctx *Context) (err error) { fmt.Println(runFuncName(), "抽奖。..") return }
func (lottery *Lottery) checkPrizesStock(ctx *Context) (err error) { fmt.Println(runFuncName(), "奖品数量判断。..") return }
func (lottery *Lottery) packagePrizeInfo(ctx *Context) (err error) { fmt.Println(runFuncName(), "组装奖品信息。..") return }
func main() { ctx := &Context{ ActInfo: &ActInfo{ ActivityType: ConstActTypeAmount, }, }
switch ctx.ActInfo.ActivityType { case ConstActTypeTime: instance := &TimeDraw{} instance.ConcreteBehavior = instance instance.Run(ctx) case ConstActTypeTimes: instance := &TimesDraw{} instance.ConcreteBehavior = instance instance.Run(ctx) case ConstActTypeAmount: instance := &AmountDraw{} instance.ConcreteBehavior = instance instance.Run(ctx) default: return } }
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 14
| [Running] go run ".../easy-tips/go/src/patterns/template/templateOther.go" main.(*Lottery).checkSerialNo 校验活动编号 (serial_no) 是否存在、并获取活动信息。.. main.(*Lottery).checkStatus 校验活动、场次是否正在进行。.. main.(*AmountDraw).checkParams 按数额范围区间抽奖:特殊参数校验。.. main.(*Lottery).checkTimesByAct 活动抽奖次数校验。.. main.(*Lottery).consumePointsByAct 活动是否需要消费积分。.. main.(*Lottery).checkTimesBySession 活动抽奖次数校验。.. main.(*Lottery).getPrizesBySession 获取场次奖品信息。.. main.(*AmountDraw).getPrizesByNode 1. 判断属于哪个数额区间。.. main.(*AmountDraw).getPrizesByNode 2. 获取对应 node 的奖品信息。.. main.(*AmountDraw).getPrizesByNode 3. 复写原所有奖品信息(抽取该 node 节点的奖品)... main.(*Lottery).drawPrizes 抽奖。.. main.(*Lottery).checkPrizesStock 奖品数量判断。.. main.(*Lottery).packagePrizeInfo 组装奖品信息。..
|
demo2 代码地址:https://github.com/TIGERB/easy-tips/blob/master/go/src/patterns/template/templateOther.go
结语
最后总结下,「模板模式」抽象过程的核心是把握不变与变:
- 不变:
Run
方法里的抽奖步骤 -> 被继承复用
- 变:不同场景下 ->
被具体实现
checkParams
参数校验逻辑
getPrizesByNode
获取该节点奖品的逻辑
作者
施展,小米信息技术部海外商城组
招聘
小米信息部武汉研发中心,信息部是小米公司整体系统规划建设的核心部门,支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作,服务小米内部所有的业务部门以及 40 家生态链公司。
同时部门承担大数据基础平台研发和微服务体系建设落,语言涉及 Java、Go,长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。
欢迎投递简历:jin.zhang(a)xiaomi.com
更多技术文章:小米信息部技术团队