代码模板 | Go设计模式实战
2020-03-30
嗯,Go设计模式实战系列,一个设计模式业务真实使用的golang系列。
前言
本系列主要分享,如何在我们的真实业务场景中使用设计模式。
本系列文章主要采用如下结构:
- 什么是「XX设计模式」?
- 什么真实业务场景可以使用「XX设计模式」?
- 怎么用「XX设计模式」?
本文主要介绍「模板模式」如何在真实业务场景中使用。
什么是「模板模式」?
抽象类里定义好算法的执行步骤和具体算法,以及可能发生变化的算法定义为抽象方法。不同的子类继承该抽象类,并实现父类的抽象方法。
模板模式的优势:
- 不变的算法被继承复用:不变的部分高度封装、复用。
- 变化的算法子类继承并具体实现:变化的部分子类只需要具体实现抽象的部分即可,方便扩展,且可无限扩展。
什么真实业务场景可以用「模板模式」?
满足如下要求的所有场景:
算法执行的步骤是稳定不变的,但是具体的某些算法可能存在变化的场景。
怎么理解,举个例子:比如说你煮个面,必然需要先烧水,水烧开之后再放面进去
,以上的流程我们称之为煮面过程
。可知:这个煮面过程
的步骤是稳定不变的,但是在不同的环境烧水的方式可能不尽相同,也许有的人用天然气烧水、有的人用电磁炉烧水、有的人用柴火烧水,等等。我们可以得到以下结论:
煮面过程
的步骤是稳定不变的
煮面过程
的烧水方式是可变的
我们有哪些真实业务场景可以用「模板模式」呢?
比如抽奖系统的抽奖接口,为什么:
- 抽奖的步骤是稳定不变的 -> 不变的算法执行步骤
- 不同抽奖类型活动在某些逻辑处理方式可能不同 -> 变的某些算法
怎么用「模板模式」?
关于怎么用,完全可以生搬硬套我总结的使用设计模式的四个步骤:
业务梳理
我通过历史上接触过的各种抽奖场景(红包雨、糖果雨、打地鼠、大转盘(九宫格)、考眼力、答题闯关、游戏闯关、支付刮刮乐、积分刮刮乐等等),按照真实业务需求梳理了以下抽奖业务抽奖接口的大致文本流程。
了解具体业务请点击《通用抽奖工具之需求分析 | SkrShop》
主步骤 |
主逻辑 |
抽奖类型 |
子步骤 |
子逻辑 |
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奖品信息
的算法是可变的
业务流程图
我们通过梳理的文本业务流程得到了如下的业务流程图:
代码建模
通过上面的分析我们可以得到:
一个抽象类 - 具体共有方法`Run`,里面定义了算法的执行步骤 - 具体私有方法,不会发生变化的具体方法 - 抽象方法,会发生变化的方法
子类一(按时间抽奖类型) - 继承抽象类父类 - 实现抽象方法
子类二(按抽奖次数抽奖类型) - 继承抽象类父类 - 实现抽象方法
子类三(按数额范围区间抽奖) - 继承抽象类父类 - 实现抽象方法
|
但是golang里面没有继承的概念,我们就把对抽象类里抽象方法的依赖转化成对接口interface
里抽象方法的依赖,同时也可以利用合成复用
的方式“继承”模板:
抽象行为的接口`BehaviorInterface`(包含如下需要实现的方法) - 其他参数校验的方法`checkParams` - 获取node奖品信息的方法`getPrizesByNode`
抽奖结构体类 - 具体共有方法`Run`,里面定义了算法的执行步骤 - 具体私有方法`checkParams` 里面的逻辑实际依赖的接口BehaviorInterface.checkParams(ctx)的抽象方法 - 具体私有方法`getPrizesByNode` 里面的逻辑实际依赖的接口BehaviorInterface.getPrizesByNode(ctx)的抽象方法 - 其他具体私有方法,不会发生变化的具体方法
实现`BehaviorInterface`的结构体一(按时间抽奖类型) - 实现接口方法
实现`BehaviorInterface`的结构体二(按抽奖次数抽奖类型) - 实现接口方法
实现`BehaviorInterface`的结构体三(按数额范围区间抽奖) - 实现接口方法
|
同时得到了我们的UML图:
代码demo
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() }
|
以下是代码执行结果:
[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的合成复用
特性实现)
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() }
|
以下是代码执行结果:
[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
获取该节点奖品的逻辑
特别说明: 1. Go设计模式实战,只是一个在代码合理设计的情况下自然而然无限接近或者达到的结果,并不是一个硬性的目标,务必较真。 2. 本系列的一些设计模式的概念可能和原概念存在差异,因为会结合实际使用,取其精华,适当改变,灵活使用。
|
文章列表
Go设计模式实战系列 更多文章 点击此处查看