【转存】Go语言设计模式
导语| 设计模式是针对软件设计中常见问题的工具箱,其中的工具就是各种经过实践验证的解决方案。即使你从未遇到过这些问题,了解模式仍然非常有用,因为它能指导你如何使用面向对象的设计原则来解决各种问题,提高开发效率,降低开发成本;本文囊括了GO语言实现的经典设计模式示例,每个示例都精心设计,力求符合模式结构,可作为日常编码参考,同时一些常用的设计模式融入了开发实践经验总结,帮助大家在平时工作中灵活运用。

责任链模式
(一)概念
责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。收到请求后,每个处理者均可对请求进行处理,或将其传递给链上的下个处理者。
该模式允许多个对象来对请求进行处理,而无需让发送者类与具体接收者类相耦合。链可在运行时由遵循标准处理者接口的任意处理者动态生成。
一般意义上的责任链模式是说,请求在链上流转时任何一个满足条件的节点处理完请求后就会停止流转并返回,不过还可以根据不同的业务情况做一些改进:
- 请求可以流经处理链的所有节点,不同节点会对请求做不同职责的处理;
- 可以通过上下文参数保存请求对象及上游节点的处理结果,供下游节点依赖,并进一步处理;
- 处理链可支持节点的异步处理,通过实现特定接口判断,是否需要异步处理;
- 责任链对于请求处理节点可以设置停止标志位,不是异常,是一种满足业务流转的中断;
- 责任链的拼接方式存在两种,一种是节点遍历,一个节点一个节点顺序执行;另一种是节点嵌套,内层节点嵌入在外层节点执行逻辑中,类似递归,或者“回”行结构;
- 责任链的节点嵌套拼接方式多被称为拦截器链或者过滤器链,更易于实现业务流程的切面,比如监控业务执行时长,日志输出,权限校验等;
(二)示例
本示例模拟实现机场登机过程,第一步办理登机牌,第二步如果有行李,就办理托运,第三步核实身份,第四步安全检查,第五步完成登机;其中行李托运是可选的,其他步骤必选,必选步骤有任何不满足就终止登机;旅客对象作为请求参数上下文,每个步骤会根据旅客对象状态判断是否处理或流转下一个节点;
(三)登机过程
package chainofresponsibilityimport "fmt"// BoardingProcessor 登机过程中,各节点统一处理接口
type BoardingProcessor interface {SetNextProcessor(processor BoardingProcessor)ProcessFor(passenger *Passenger)
}// Passenger 旅客
type Passenger struct {name string // 姓名hasBoardingPass bool // 是否办理登机牌hasLuggage bool // 是否有行李需要托运isPassIdentityCheck bool // 是否通过身份校验isPassSecurityCheck bool // 是否通过安检isCompleteForBoarding bool // 是否完成登机
}// baseBoardingProcessor 登机流程处理器基类
type baseBoardingProcessor struct {// nextProcessor 下一个登机处理流程nextProcessor BoardingProcessor
}// SetNextProcessor 基类中统一实现设置下一个处理器方法
func (b *baseBoardingProcessor) SetNextProcessor(processor BoardingProcessor) {b.nextProcessor = processor
}// ProcessFor 基类中统一实现下一个处理器流转
func (b *baseBoardingProcessor) ProcessFor(passenger *Passenger) {if b.nextProcessor != nil {b.nextProcessor.ProcessFor(passenger)}
}// boardingPassProcessor 办理登机牌处理器
type boardingPassProcessor struct {baseBoardingProcessor // 引用基类
}func (b *boardingPassProcessor) ProcessFor(passenger *Passenger) {if !passenger.hasBoardingPass {fmt.Printf("为旅客%s办理登机牌;\n", passenger.name)passenger.hasBoardingPass = true}// 成功办理登机牌后,进入下一个流程处理b.baseBoardingProcessor.ProcessFor(passenger)
}// luggageCheckInProcessor 托运行李处理器
type luggageCheckInProcessor struct {baseBoardingProcessor
}func (l *luggageCheckInProcessor) ProcessFor(passenger *Passenger) {if !passenger.hasBoardingPass {fmt.Printf("旅客%s未办理登机牌,不能托运行李;\n", passenger.name)return}if passenger.hasLuggage {fmt.Printf("为旅客%s办理行李托运;\n", passenger.name)}l.baseBoardingProcessor.ProcessFor(passenger)
}// identityCheckProcessor 校验身份处理器
type identityCheckProcessor struct {baseBoardingProcessor
}func (i *identityCheckProcessor) ProcessFor(passenger *Passenger) {if !passenger.hasBoardingPass {fmt.Printf("旅客%s未办理登机牌,不能办理身份校验;\n", passenger.name)return}if !passenger.isPassIdentityCheck {fmt.Printf("为旅客%s核实身份信息;\n", passenger.name)passenger.isPassIdentityCheck = true}i.baseBoardingProcessor.ProcessFor(passenger)
}// securityCheckProcessor 安检处理器
type securityCheckProcessor struct {baseBoardingProcessor
}func (s *securityCheckProcessor) ProcessFor(passenger *Passenger) {if !passenger.hasBoardingPass {fmt.Printf("旅客%s未办理登机牌,不能进行安检;\n", passenger.name)return}if !passenger.isPassSecurityCheck {fmt.Printf("为旅客%s进行安检;\n", passenger.name)passenger.isPassSecurityCheck = true}s.baseBoardingProcessor.ProcessFor(passenger)
}// completeBoardingProcessor 完成登机处理器
type completeBoardingProcessor struct {baseBoardingProcessor
}func (c *completeBoardingProcessor) ProcessFor(passenger *Passenger) {if !passenger.hasBoardingPass ||!passenger.isPassIdentityCheck ||!passenger.isPassSecurityCheck {fmt.Printf("旅客%s登机检查过程未完成,不能登机;\n", passenger.name)return}passenger.isCompleteForBoarding = truefmt.Printf("旅客%s成功登机;\n", passenger.name)
}
(四)测试程序
package chainofresponsibilityimport "testing"func TestChainOfResponsibility(t *testing.T) {boardingProcessor := BuildBoardingProcessorChain()passenger := &Passenger{name: "李四",hasBoardingPass: false,hasLuggage: true,isPassIdentityCheck: false,isPassSecurityCheck: false,isCompleteForBoarding: false,}boardingProcessor.ProcessFor(passenger)
}// BuildBoardingProcessorChain 构建登机流程处理链
func BuildBoardingProcessorChain() BoardingProcessor {completeBoardingNode := &completeBoardingProcessor{}securityCheckNode := &securityCheckProcessor{}securityCheckNode.SetNextProcessor(completeBoardingNode)identityCheckNode := &identityCheckProcessor{}identityCheckNode.SetNextProcessor(securityCheckNode)luggageCheckInNode := &luggageCheckInProcessor{}luggageCheckInNode.SetNextProcessor(identityCheckNode)boardingPassNode := &boardingPassProcessor{}boardingPassNode.SetNextProcessor(luggageCheckInNode)return boardingPassNode
}
(五)运行结果
=== RUN TestChainOfResponsibility
为旅客李四办理登机牌;
为旅客李四办理行李托运;
为旅客李四核实身份信息;
为旅客李四进行安检;
旅客李四成功登机;
--- PASS: TestChainOfResponsibility (0.00s)
PASS

命令模式
(一)概念
命令模式是一种行为设计模式,它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作。
方法参数化是指将每个请求参数传入具体命令的工厂方法(go语言没有构造函数)创建命令,同时具体命令会默认设置好接受对象,这样做的好处是不管请求参数个数及类型,还是接受对象有几个,都会被封装到具体命令对象的成员字段上,并通过统一的Execute接口方法进行调用,屏蔽各个请求的差异,便于命令扩展,多命令组装,回滚等;
(二)示例
控制电饭煲做饭是一个典型的命令模式的场景,电饭煲的控制面板会提供设置煮粥、蒸饭模式,及开始和停止按钮,电饭煲控制系统会根据模式的不同设置相应的火力,压强及时间等参数;煮粥,蒸饭就相当于不同的命令,开始按钮就相当命令触发器,设置好做饭模式,点击开始按钮电饭煲就开始运行,同时还支持停止命令;
(三)电饭煲接收器
package commandimport "fmt"// ElectricCooker 电饭煲
type ElectricCooker struct {fire string // 火力pressure string // 压力
}// SetFire 设置火力
func (e *ElectricCooker) SetFire(fire string) {e.fire = fire
}// SetPressure 设置压力
func (e *ElectricCooker) SetPressure(pressure string) {e.pressure = pressure
}// Run 持续运行指定时间
func (e *ElectricCooker) Run(duration string) string {return fmt.Sprintf("电饭煲设置火力为%s,压力为%s,持续运行%s;", e.fire, e.pressure, duration)
}// Shutdown 停止
func (e *ElectricCooker) Shutdown() string {return "电饭煲停止运行。"
}
(四)电饭煲命令
package command// CookCommand 做饭指令接口
type CookCommand interface {Execute() string // 指令执行方法
}// steamRiceCommand 蒸饭指令
type steamRiceCommand struct {electricCooker *ElectricCooker // 电饭煲
}func NewSteamRiceCommand(electricCooker *ElectricCooker) *steamRiceCommand {return &steamRiceCommand{electricCooker: electricCooker,}
}func (s *steamRiceCommand) Execute() string {s.electricCooker.SetFire("中")s.electricCooker.SetPressure("正常")return "蒸饭:" + s.electricCooker.Run("30分钟")
}// cookCongeeCommand 煮粥指令
type cookCongeeCommand struct {electricCooker *ElectricCooker
}func NewCookCongeeCommand(electricCooker *ElectricCooker) *cookCongeeCommand {return &cookCongeeCommand{electricCooker: electricCooker,}
}func (c *cookCongeeCommand) Execute() string {c.electricCooker.SetFire("大")c.electricCooker.SetPressure("强")return "煮粥:" + c.electricCooker.Run("45分钟")
}// shutdownCommand 停止指令
type shutdownCommand struct {electricCooker *ElectricCooker
}func NewShutdownCommand(electricCooker *ElectricCooker) *shutdownCommand {return &shutdownCommand{electricCooker: electricCooker,}
}func (s *shutdownCommand) Execute() string {return s.electricCooker.Shutdown()
}// ElectricCookerInvoker 电饭煲指令触发器
type ElectricCookerInvoker struct {cookCommand CookCommand
}// SetCookCommand 设置指令
func (e *ElectricCookerInvoker) SetCookCommand(cookCommand CookCommand) {e.cookCommand = cookCommand
}// ExecuteCookCommand 执行指令
func (e *ElectricCookerInvoker) ExecuteCookCommand() string {return e.cookCommand.Execute()
}
(五)测试程序
package commandimport ("fmt""testing"
)func TestCommand(t *testing.T) {// 创建电饭煲,命令接受者electricCooker := new(ElectricCooker)// 创建电饭煲指令触发器electricCookerInvoker := new(ElectricCookerInvoker)// 蒸饭steamRiceCommand := NewSteamRiceCommand(electricCooker)electricCookerInvoker.SetCookCommand(steamRiceCommand)fmt.Println(electricCookerInvoker.ExecuteCookCommand())// 煮粥cookCongeeCommand := NewCookCongeeCommand(electricCooker)electricCookerInvoker.SetCookCommand(cookCongeeCommand)fmt.Println(electricCookerInvoker.ExecuteCookCommand())// 停止shutdownCommand := NewShutdownCommand(electricCooker)electricCookerInvoker.SetCookCommand(shutdownCommand)fmt.Println(electricCookerInvoker.ExecuteCookCommand())
}
(六)运行结果
=== RUN TestCommand
蒸饭:电饭煲设置火力为中,压力为正常,持续运行30分钟;
煮粥:电饭煲设置火力为大,压力为强,持续运行45分钟;
电饭煲停止运行。
--- PASS: TestCommand (0.00s)
PASS

迭代器模式
(一)概念
迭代器模式是一种行为设计模式,让你能在不暴露集合底层表现形式 (列表、 栈和树等)的情况下遍历集合中所有的元素。
在迭代器的帮助下, 客户端可以用一个迭代器接口以相似的方式遍历不同集合中的元素。
这里需要注意的是有两个典型的迭代器接口需要分清楚;一个是集合类实现的可以创建迭代器的工厂方法接口一般命名为Iterable,包含的方法类似CreateIterator;另一个是迭代器本身的接口,命名为Iterator,有Next及hasMore两个主要方法;
(二)示例
一个班级类中包括一个老师和若干个学生,我们要对班级所有成员进行遍历,班级中老师存储在单独的结构字段中,学生存储在另外一个slice字段中,通过迭代器,我们实现统一遍历处理;
(三)班级成员
package iteratorimport "fmt"// Member 成员接口
type Member interface {Desc() string // 输出成员描述信息
}// Teacher 老师
type Teacher struct {name string // 名称subject string // 所教课程
}// NewTeacher 根据姓名、课程创建老师对象
func NewTeacher(name, subject string) *Teacher {return &Teacher{name: name,subject: subject,}
}func (t *Teacher) Desc() string {return fmt.Sprintf("%s班主任老师负责教%s", t.name, t.subject)
}// Student 学生
type Student struct {name string // 姓名sumScore int // 考试总分数
}// NewStudent 创建学生对象
func NewStudent(name string, sumScore int) *Student {return &Student{name: name,sumScore: sumScore,}
}func (t *Student) Desc() string {return fmt.Sprintf("%s同学考试总分为%d", t.name, t.sumScore)
}
(四)班级成员迭代器
package iterator// Iterator 迭代器接口
type Iterator interface {Next() Member // 迭代下一个成员HasMore() bool // 是否还有
}// memberIterator 班级成员迭代器实现
type memberIterator struct {class *Class // 需迭代的班级index int // 迭代索引
}func (m *memberIterator) Next() Member {// 迭代索引为-1时,返回老师成员,否则遍历学生sliceif m.index == -1 {m.index++return m.class.teacher}student := m.class.students[m.index]m.index++return student
}func (m *memberIterator) HasMore() bool {return m.index < len(m.class.students)
}// Iterable 可迭代集合接口,实现此接口返回迭代器
type Iterable interface {CreateIterator() Iterator
}// Class 班级,包括老师和同学
type Class struct {name stringteacher *Teacherstudents []*Student
}// NewClass 根据班主任老师名称,授课创建班级
func NewClass(name, teacherName, teacherSubject string) *Class {return &Class{name: name,teacher: NewTeacher(teacherName, teacherSubject),}
}// CreateIterator 创建班级迭代器
func (c *Class) CreateIterator() Iterator {return &memberIterator{class: c,index: -1, // 迭代索引初始化为-1,从老师开始迭代}
}func (c *Class) Name() string {return c.name
}// AddStudent 班级添加同学
func (c *Class) AddStudent(students ...*Student) {c.students = append(c.students, students...)
}
(五)测试程序
package iteratorimport ("fmt""testing"
)func TestIterator(t *testing.T) {class := NewClass("三年级一班", "王明", "数学课")class.AddStudent(NewStudent("张三", 389),NewStudent("李四", 378),NewStudent("王五", 347))fmt.Printf("%s成员如下:\n", class.Name())classIterator := class.CreateIterator()for classIterator.HasMore() {member := classIterator.Next()fmt.Println(member.Desc())}
}
(六)运行结果
=== RUN TestIterator
三年级一班成员如下:
王明班主任老师负责教数学课
张三同学考试总分为389
李四同学考试总分为378
王五同学考试总分为347
--- PASS: TestIterator (0.00s)
PASS

中介者模式
(一)概念
中介者模式是一种行为设计模式,能让你减少对象之间混乱无序的依赖关系。该模式会限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作,将网状依赖变为星状依赖。
中介者能使得程序更易于修改和扩展,而且能更方便地对独立的组件进行复用,因为它们不再依赖于很多其他的类。
中介者模式与观察者模式之间的区别是,中介者模式解决的是同类或者不同类的多个对象之间多对多的依赖关系,观察者模式解决的是多个对象与一个对象之间的多对一的依赖关系。
(二)示例
机场塔台调度系统是一个体现中介者模式的典型示例,假设是一个小机场,每次只能同时允许一架飞机起降,每架靠近机场的飞机需要先与塔台沟通是否可以降落,如果没有空闲的跑道,需要在天空盘旋等待,如果有飞机离港,等待的飞机会收到塔台的通知,按先后顺序降落;这种方式,免去多架飞机同时到达机场需要相互沟通降落顺序的复杂性,减少多个飞机间的依赖关系,简化业务逻辑,从而降低系统出问题的风险。
(三)飞机对象
package mediatorimport "fmt"// Aircraft 飞机接口
type Aircraft interface {ApproachAirport() // 抵达机场空域DepartAirport() // 飞离机场
}// airliner 客机
type airliner struct {name string // 客机型号airportMediator AirportMediator // 机场调度
}// NewAirliner 根据指定型号及机场调度创建客机
func NewAirliner(name string, mediator AirportMediator) *airliner {return &airliner{name: name,airportMediator: mediator,}
}func (a *airliner) ApproachAirport() {if !a.airportMediator.CanLandAirport(a) { // 请求塔台是否可以降落fmt.Printf("机场繁忙,客机%s继续等待降落;\n", a.name)return}fmt.Printf("客机%s成功滑翔降落机场;\n", a.name)
}func (a *airliner) DepartAirport() {fmt.Printf("客机%s成功滑翔起飞,离开机场;\n", a.name)a.airportMediator.NotifyWaitingAircraft() // 通知等待的其他飞机
}// helicopter 直升机
type helicopter struct {name stringairportMediator AirportMediator
}// NewHelicopter 根据指定型号及机场调度创建直升机
func NewHelicopter(name string, mediator AirportMediator) *helicopter {return &helicopter{name: name,airportMediator: mediator,}
}func (h *helicopter) ApproachAirport() {if !h.airportMediator.CanLandAirport(h) { // 请求塔台是否可以降落fmt.Printf("机场繁忙,直升机%s继续等待降落;\n", h.name)return}fmt.Printf("直升机%s成功垂直降落机场;\n", h.name)
}func (h *helicopter) DepartAirport() {fmt.Printf("直升机%s成功垂直起飞,离开机场;\n", h.name)h.airportMediator.NotifyWaitingAircraft() // 通知其他等待降落的飞机
}
(四)机场塔台
package mediator// AirportMediator 机场调度中介者
type AirportMediator interface {CanLandAirport(aircraft Aircraft) bool // 确认是否可以降落NotifyWaitingAircraft() // 通知等待降落的其他飞机
}// ApproachTower 机场塔台
type ApproachTower struct {hasFreeAirstrip boolwaitingQueue []Aircraft // 等待降落的飞机队列
}func (a *ApproachTower) CanLandAirport(aircraft Aircraft) bool {if a.hasFreeAirstrip {a.hasFreeAirstrip = falsereturn true}// 没有空余的跑道,加入等待队列a.waitingQueue = append(a.waitingQueue, aircraft)return false
}func (a *ApproachTower) NotifyWaitingAircraft() {if !a.hasFreeAirstrip {a.hasFreeAirstrip = true}if len(a.waitingQueue) > 0 {// 如果存在等待降落的飞机,通知第一个降落first := a.waitingQueue[0]a.waitingQueue = a.waitingQueue[1:]first.ApproachAirport()}
}
(五)测试程序
package mediatorimport "testing"func TestMediator(t *testing.T) {// 创建机场调度塔台airportMediator := &ApproachTower{hasFreeAirstrip: true}// 创建C919客机c919Airliner := NewAirliner("C919", airportMediator)// 创建米-26重型运输直升机m26Helicopter := NewHelicopter("米-26", airportMediator)c919Airliner.ApproachAirport() // c919进港降落m26Helicopter.ApproachAirport() // 米-26进港等待c919Airliner.DepartAirport() // c919飞离,等待的米-26进港降落m26Helicopter.DepartAirport() // 最后米-26飞离
}
(六)运行结果
=== RUN TestMediator
客机C919成功滑翔降落机场;
机场繁忙,直升机米-26继续等待降落;
客机C919成功滑翔起飞,离开机场;
直升机米-26成功垂直降落机场;
直升机米-26成功垂直起飞,离开机场;
--- PASS: TestMediator (0.00s)
PASS

备忘录模式
(一)概念
备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
备忘录不会影响它所处理的对象的内部结构, 也不会影响快照中保存的数据。
一般情况由原发对象保存生成的备忘录对象的状态不能被除原发对象之外的对象访问,所以通过内部类定义具体的备忘录对象是比较安全的,但是go语言不支持内部类定义的方式,因此go语言实现备忘录对象时,首先将备忘录保存的状态设为非导出字段,避免外部对象访问,其次将原发对象的引用保存到备忘录对象中,当通过备忘录对象恢复时,直接操作备忘录的恢复方法,将备份数据状态设置到原发对象中,完成恢复。
(二)示例
大家平时玩的角色扮演闯关游戏的存档机制就可以通过备忘录模式实现,每到一个关键关卡,玩家经常会先保存游戏存档,用于闯关失败后重置,存档会把角色状态及场景状态保存到备忘录中,同时将需要恢复游戏的引用存入备忘录,用于关卡重置;
(三)闯关游戏
package mementoimport "fmt"// Originator 备忘录模式原发器接口
type Originator interface {Save(tag string) Memento // 当前状态保存备忘录
}// RolesPlayGame 支持存档的RPG游戏
type RolesPlayGame struct {name string // 游戏名称rolesState []string // 游戏角色状态scenarioState string // 游戏场景状态
}// NewRolesPlayGame 根据游戏名称和角色名,创建RPG游戏
func NewRolesPlayGame(name string, roleName string) *RolesPlayGame {return &RolesPlayGame{name: name,rolesState: []string{roleName, "血量100"}, // 默认满血scenarioState: "开始通过第一关", // 默认第一关开始}
}// Save 保存RPG游戏角色状态及场景状态到指定标签归档
func (r *RolesPlayGame) Save(tag string) Memento {return newRPGArchive(tag, r.rolesState, r.scenarioState, r)
}func (r *RolesPlayGame) SetRolesState(rolesState []string) {r.rolesState = rolesState
}func (r *RolesPlayGame) SetScenarioState(scenarioState string) {r.scenarioState = scenarioState
}// String 输出RPG游戏简要信息
func (r *RolesPlayGame) String() string {return fmt.Sprintf("在%s游戏中,玩家使用%s,%s,%s;", r.name, r.rolesState[0], r.rolesState[1], r.scenarioState)
}
(四)游戏存档
package mementoimport "fmt"// Memento 备忘录接口
type Memento interface {Tag() string // 备忘录标签Restore() // 根据备忘录存储数据状态恢复原对象
}// rpgArchive rpg游戏存档,
type rpgArchive struct {tag string // 存档标签rolesState []string // 存档的角色状态scenarioState string // 存档游戏场景状态rpg *RolesPlayGame // rpg游戏引用
}// newRPGArchive 根据标签,角色状态,场景状态,rpg游戏引用,创建游戏归档备忘录
func newRPGArchive(tag string, rolesState []string, scenarioState string, rpg *RolesPlayGame) *rpgArchive {return &rpgArchive{tag: tag,rolesState: rolesState,scenarioState: scenarioState,rpg: rpg,}
}func (r *rpgArchive) Tag() string {return r.tag
}// Restore 根据归档数据恢复游戏状态
func (r *rpgArchive) Restore() {r.rpg.SetRolesState(r.rolesState)r.rpg.SetScenarioState(r.scenarioState)
}// RPGArchiveManager RPG游戏归档管理器
type RPGArchiveManager struct {archives map[string]Memento // 存储归档标签对应归档
}func NewRPGArchiveManager() *RPGArchiveManager {return &RPGArchiveManager{archives: make(map[string]Memento),}
}// Reload 根据标签重新加载归档数据
func (r *RPGArchiveManager) Reload(tag string) {if archive, ok := r.archives[tag]; ok {fmt.Printf("重新加载%s;\n", tag)archive.Restore()}
}// Put 保存归档数据
func (r *RPGArchiveManager) Put(memento Memento) {r.archives[memento.Tag()] = memento
}
(五)测试程序
package mementoimport ("fmt""testing"
)func TestMemento(t *testing.T) {// 创建RPG游戏存档管理器rpgManager := NewRPGArchiveManager()// 创建RPG游戏rpg := NewRolesPlayGame("暗黑破坏神2", "野蛮人战士")fmt.Println(rpg) // 输出游戏当前状态rpgManager.Put(rpg.Save("第一关存档")) // 游戏存档// 第一关闯关失败rpg.SetRolesState([]string{"野蛮人战士", "死亡"})rpg.SetScenarioState("第一关闯关失败")fmt.Println(rpg)// 恢复存档,重新闯关rpgManager.Reload("第一关存档")fmt.Println(rpg)
}
(六)运行结果
=== RUN TestMemento
在暗黑破坏神2游戏中,玩家使用野蛮人战士,血量100,开始通过第一关;
在暗黑破坏神2游戏中,玩家使用野蛮人战士,死亡,第一关闯关失败;
重新加载第一关存档;
在暗黑破坏神2游戏中,玩家使用野蛮人战士,血量100,开始通过第一关;
--- PASS: TestMemento (0.00s)
PASS

观察者模式
(一)概念
观察者模式是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个 “观察” 该对象的其他对象。
观察者模式提供了一种作用于任何实现了订阅者接口的对象的机制,可对其事件进行订阅和取消订阅。
观察者模式是最常用的模式之一,是事件总线,分布式消息中间件等各种事件机制的原始理论基础,常用于解耦多对一的对象依赖关系;
增强的实现功能包括:
- 当被观察者通过异步实现通知多个观察者时就相当于单进程实例的消息总线;
- 同时还可以根据业务需要,将被观察者所有数据状态变更进行分类为不同的主题,观察者通过不同主题进行订阅;
- 同一个主题又可分为增加,删除,修改事件行为;
- 每个主题可以实现一个线程池,多个主题通过不同的线程池进行处理隔离,线程池可以设置并发线程大小、缓冲区大小及调度策略,比如先进先出,优先级等策略;
- 观察者处理事件时有可能出现异常,所以也可以注册异常处理函数,异常处理也可以通过异常类型进行分类;
- 根据业务需求也可以实现通知异常重试,延迟通知等功能;
(二)示例
信用卡业务消息提醒可通过观察者模式实现,业务消息包括日常消费,出账单,账单逾期,消息提醒包括短信、邮件及电话,根据不同业务的场景会采用不同的消息提醒方式或者多种消息提醒方式,这里信用卡相当于被观察者,观察者相当于不同的通知方式;日常消费通过短信通知,出账单通过邮件通知,账单逾期三种方式都会进行通知;
(三)通知方式
package observerimport "fmt"// Subscriber 订阅者接口
type Subscriber interface {Name() string //订阅者名称Update(message string) //订阅更新方法
}// shortMessage 信用卡消息短信订阅者
type shortMessage struct{}func (s *shortMessage) Name() string {return "手机短息"
}func (s *shortMessage) Update(message string) {fmt.Printf("通过【%s】发送消息:%s\n", s.Name(), message)
}// email 信用卡消息邮箱订阅者
type email struct{}func (e *email) Name() string {return "电子邮件"
}func (e *email) Update(message string) {fmt.Printf("通过【%s】发送消息:%s\n", e.Name(), message)
}// telephone 信用卡消息电话订阅者
type telephone struct{}func (t *telephone) Name() string {return "电话"
}func (t *telephone) Update(message string) {fmt.Printf("通过【%s】告知:%s\n", t.Name(), message)
}
(四)信用卡业务
package observerimport "fmt"// MsgType 信用卡消息类型
type MsgType intconst (ConsumeType MsgType = iota // 消费消息类型BillType // 账单消息类型ExpireType // 逾期消息类型
)// CreditCard 信用卡
type CreditCard struct {holder string // 持卡人consumeSum float32 // 消费总金额subscriberGroup map[MsgType][]Subscriber // 根据消息类型分组订阅者
}// NewCreditCard 指定持卡人创建信用卡
func NewCreditCard(holder string) *CreditCard {return &CreditCard{holder: holder,subscriberGroup: make(map[MsgType][]Subscriber),}
}// Subscribe 支持订阅多种消息类型
func (c *CreditCard) Subscribe(subscriber Subscriber, msgTypes ...MsgType) {for _, msgType := range msgTypes {c.subscriberGroup[msgType] = append(c.subscriberGroup[msgType], subscriber)}
}// Unsubscribe 解除订阅多种消息类型
func (c *CreditCard) Unsubscribe(subscriber Subscriber, msgTypes ...MsgType) {for _, msgType := range msgTypes {if subs, ok := c.subscriberGroup[msgType]; ok {c.subscriberGroup[msgType] = removeSubscriber(subs, subscriber)}}
}func removeSubscriber(subscribers []Subscriber, toRemove Subscriber) []Subscriber {length := len(subscribers)for i, subscriber := range subscribers {if toRemove.Name() == subscriber.Name() {subscribers[length-1], subscribers[i] = subscribers[i], subscribers[length-1]return subscribers[:length-1]}}return subscribers
}// Consume 信用卡消费
func (c *CreditCard) Consume(money float32) {c.consumeSum += moneyc.notify(ConsumeType, fmt.Sprintf("尊敬的持卡人%s,您当前消费%.2f元;", c.holder, money))
}// SendBill 发送信用卡账单
func (c *CreditCard) SendBill() {c.notify(BillType, fmt.Sprintf("尊敬的持卡人%s,您本月账单已出,消费总额%.2f元;", c.holder, c.consumeSum))
}// Expire 逾期通知
func (c *CreditCard) Expire() {c.notify(ExpireType, fmt.Sprintf("尊敬的持卡人%s,您本月账单已逾期,请及时还款,总额%.2f元;", c.holder, c.consumeSum))
}// notify 根据消息类型通知订阅者
func (c *CreditCard) notify(msgType MsgType, message string) {if subs, ok := c.subscriberGroup[msgType]; ok {for _, sub := range subs {sub.Update(message)}}
}
(五)测试程序
package observerimport "testing"func TestObserver(t *testing.T) {// 创建张三的信用卡creditCard := NewCreditCard("张三")// 短信通知订阅信用卡消费及逾期消息creditCard.Subscribe(new(shortMessage), ConsumeType, ExpireType)// 电子邮件通知订阅信用卡账单及逾期消息creditCard.Subscribe(new(email), BillType, ExpireType)// 电话通知订阅信用卡逾期消息,同时逾期消息通过三种方式通知creditCard.Subscribe(new(telephone), ExpireType)creditCard.Consume(500.00) // 信用卡消费creditCard.Consume(800.00) // 信用卡消费creditCard.SendBill() // 信用卡发送账单creditCard.Expire() // 信用卡逾期// 信用卡逾期消息取消电子邮件及短信通知订阅creditCard.Unsubscribe(new(email), ExpireType)creditCard.Unsubscribe(new(shortMessage), ExpireType)creditCard.Consume(300.00) // 信用卡消费creditCard.Expire() // 信用卡逾期
}
(六)运行结果
=== RUN TestObserver
通过【手机短息】发送消息:尊敬的持卡人张三,您当前消费500.00元;
通过【手机短息】发送消息:尊敬的持卡人张三,您当前消费800.00元;
通过【电子邮件】发送消息:尊敬的持卡人张三,您本月账单已出,消费总额1300.00元;
通过【手机短息】发送消息:尊敬的持卡人张三,您本月账单已逾期,请及时还款,总额1300.00元;
通过【电子邮件】发送消息:尊敬的持卡人张三,您本月账单已逾期,请及时还款,总额1300.00元;
通过【电话】告知:尊敬的持卡人张三,您本月账单已逾期,请及时还款,总额1300.00元;
通过【手机短息】发送消息:尊敬的持卡人张三,您当前消费300.00元;
通过【电话】告知:尊敬的持卡人张三,您本月账单已逾期,请及时还款,总额1600.00元;
--- PASS: TestObserver (0.00s)
PASS

状态模式
(一)概念
状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。
该模式将与状态相关的行为抽取到独立的状态类中,让原对象将工作委派给这些类的实例,而不是自行进行处理。
状态迁移有四个元素组成,起始状态、触发迁移的事件,终止状态以及要执行的动作,每个具体的状态包含触发状态迁移的执行方法,迁移方法的实现是执行持有状态对象的动作方法,同时设置状态为下一个流转状态;持有状态的业务对象包含有触发状态迁移方法,这些迁移方法将请求委托给当前具体状态对象的迁移方法。
(二)示例
IPhone手机充电就是一个手机电池状态的流转,一开始手机处于有电状态,插入充电插头后,继续充电到满电状态,并进入断电保护,拔出充电插头后使用手机,由满电逐渐变为没电,最终关机;
状态迁移表:
起始状态 | 触发事件 | 终止状态 | 执行动作 |
有电 | 插入充电线 | 满电 | 充电 |
有电 | 拔出充电线 | 没电 | 耗电 |
满电 | 插入充电线 | 满电 | 停止充电 |
满电 | 拔出充电线 | 有电 | 耗电 |
没电 | 插入充电线 | 有电 | 充电 |
没电 | 拔出充电线 | 没电 | 关机 |
(三)电池状态
package stateimport "fmt"// BatteryState 电池状态接口,支持手机充电线插拔事件
type BatteryState interface {ConnectPlug(iPhone *IPhone) stringDisconnectPlug(iPhone *IPhone) string
}// fullBatteryState 满电状态
type fullBatteryState struct{}func (s *fullBatteryState) String() string {return "满电状态"
}func (s *fullBatteryState) ConnectPlug(iPhone *IPhone) string {return iPhone.pauseCharge()
}func (s *fullBatteryState) DisconnectPlug(iPhone *IPhone) string {iPhone.SetBatteryState(PartBatteryState)return fmt.Sprintf("%s,%s转为%s", iPhone.consume(), s, PartBatteryState)
}// emptyBatteryState 空电状态
type emptyBatteryState struct{}func (s *emptyBatteryState) String() string {return "没电状态"
}func (s *emptyBatteryState) ConnectPlug(iPhone *IPhone) string {iPhone.SetBatteryState(PartBatteryState)return fmt.Sprintf("%s,%s转为%s", iPhone.charge(), s, PartBatteryState)
}func (s *emptyBatteryState) DisconnectPlug(iPhone *IPhone) string {return iPhone.shutdown()
}// partBatteryState 部分电状态
type partBatteryState struct{}func (s *partBatteryState) String() string {return "有电状态"
}func (s *partBatteryState) ConnectPlug(iPhone *IPhone) string {iPhone.SetBatteryState(FullBatteryState)return fmt.Sprintf("%s,%s转为%s", iPhone.charge(), s, FullBatteryState)
}func (s *partBatteryState) DisconnectPlug(iPhone *IPhone) string {iPhone.SetBatteryState(EmptyBatteryState)return fmt.Sprintf("%s,%s转为%s", iPhone.consume(), s, EmptyBatteryState)
}
(四)IPhone手机
package stateimport "fmt"// 电池状态单例,全局统一使用三个状态的单例,不需要重复创建
var (FullBatteryState = new(fullBatteryState) // 满电EmptyBatteryState = new(emptyBatteryState) // 空电PartBatteryState = new(partBatteryState) // 部分电
)// IPhone 已手机充电为例,实现状态模式
type IPhone struct {model string // 手机型号batteryState BatteryState // 电池状态
}// NewIPhone 创建指定型号手机
func NewIPhone(model string) *IPhone {return &IPhone{model: model,batteryState: PartBatteryState,}
}// BatteryState 输出电池当前状态
func (i *IPhone) BatteryState() string {return fmt.Sprintf("iPhone %s 当前为%s", i.model, i.batteryState)
}// ConnectPlug 连接充电线
func (i *IPhone) ConnectPlug() string {return fmt.Sprintf("iPhone %s 连接电源线,%s", i.model, i.batteryState.ConnectPlug(i))
}// DisconnectPlug 断开充电线
func (i *IPhone) DisconnectPlug() string {return fmt.Sprintf("iPhone %s 断开电源线,%s", i.model, i.batteryState.DisconnectPlug(i))
}// SetBatteryState 设置电池状态
func (i *IPhone) SetBatteryState(state BatteryState) {i.batteryState = state
}func (i *IPhone) charge() string {return "正在充电"
}func (i *IPhone) pauseCharge() string {return "电已满,暂停充电"
}func (i *IPhone) shutdown() string {return "手机关闭"
}func (i *IPhone) consume() string {return "使用中,消耗电量"
}
(五)测试程序
package stateimport ("fmt""testing"
)func TestState(t *testing.T) {iPhone13Pro := NewIPhone("13 pro") // 刚创建的手机有部分电fmt.Println(iPhone13Pro.BatteryState()) // 打印部分电状态fmt.Println(iPhone13Pro.ConnectPlug()) // 插上电源插头,继续充满电fmt.Println(iPhone13Pro.ConnectPlug()) // 满电后再充电,会触发满电保护fmt.Println(iPhone13Pro.DisconnectPlug()) // 拔掉电源,使用手机消耗电量,变为有部分电fmt.Println(iPhone13Pro.DisconnectPlug()) // 一直使用手机,直到没电fmt.Println(iPhone13Pro.DisconnectPlug()) // 没电后会关机fmt.Println(iPhone13Pro.ConnectPlug()) // 再次插上电源一会,变为有电状态
}
(六)运行结果
=== RUN TestState
iPhone 13 pro 当前为有电状态
iPhone 13 pro 连接电源线,正在充电,有电状态转为满电状态
iPhone 13 pro 连接电源线,电已满,暂停充电
iPhone 13 pro 断开电源线,使用中,消耗电量,满电状态转为有电状态
iPhone 13 pro 断开电源线,使用中,消耗电量,有电状态转为没电状态
iPhone 13 pro 断开电源线,手机关闭
iPhone 13 pro 连接电源线,正在充电,没电状态转为有电状态
--- PASS: TestState (0.00s)
PASS

策略模式
(一)概念
策略模式是一种行为设计模式,它能让你定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换。
原始对象被称为上下文,它包含指向策略对象的引用并将执行行为的任务分派给策略对象。为了改变上下文完成其工作的方式,其他对象可以使用另一个对象来替换当前链接的策略对象。
策略模式是最常用的设计模式,也是比较简单的设计模式,是以多态替换条件表达式重构方法的具体实现,是面向接口编程原则的最直接体现;
(二)示例
北京是一个四季分明的城市,每个季节天气情况都有明显特点;我们定义一个显示天气情况的季节接口,具体的四季实现,都会保存一个城市和天气情况的映射表,城市对象会包含季节接口,随着四季的变化,天气情况也随之变化;
(三)四季天气
package strategyimport "fmt"// Season 季节的策略接口,不同季节表现得天气不同
type Season interface {ShowWeather(city string) string // 显示指定城市的天气情况
}type spring struct {weathers map[string]string // 存储不同城市春天气候
}func NewSpring() *spring {return &spring{weathers: map[string]string{"北京": "干燥多风", "昆明": "清凉舒适"},}
}func (s *spring) ShowWeather(city string) string {return fmt.Sprintf("%s的春天,%s;", city, s.weathers[city])
}type summer struct {weathers map[string]string // 存储不同城市夏天气候
}func NewSummer() *summer {return &summer{weathers: map[string]string{"北京": "高温多雨", "昆明": "清凉舒适"},}
}func (s *summer) ShowWeather(city string) string {return fmt.Sprintf("%s的夏天,%s;", city, s.weathers[city])
}type autumn struct {weathers map[string]string // 存储不同城市秋天气候
}func NewAutumn() *autumn {return &autumn{weathers: map[string]string{"北京": "凉爽舒适", "昆明": "清凉舒适"},}
}func (a *autumn) ShowWeather(city string) string {return fmt.Sprintf("%s的秋天,%s;", city, a.weathers[city])
}type winter struct {weathers map[string]string // 存储不同城市冬天气候
}func NewWinter() *winter {return &winter{weathers: map[string]string{"北京": "干燥寒冷", "昆明": "清凉舒适"},}
}func (w *winter) ShowWeather(city string) string {return fmt.Sprintf("%s的冬天,%s;", city, w.weathers[city])
}
(四)城市气候
package strategyimport ("fmt"
)// City 城市
type City struct {name stringfeature stringseason Season
}// NewCity 根据名称及季候特征创建城市
func NewCity(name, feature string) *City {return &City{name: name,feature: feature,}
}// SetSeason 设置不同季节,类似天气在不同季节的不同策略
func (c *City) SetSeason(season Season) {c.season = season
}// String 显示城市的气候信息
func (c *City) String() string {return fmt.Sprintf("%s%s,%s", c.name, c.feature, c.season.ShowWeather(c.name))
}
(五)测试程序
package strategyimport ("fmt""testing"
)func TestStrategy(t *testing.T) {Beijing := NewCity("北京", "四季分明")Beijing.SetSeason(NewSpring())fmt.Println(Beijing)Beijing.SetSeason(NewSummer())fmt.Println(Beijing)Beijing.SetSeason(NewAutumn())fmt.Println(Beijing)Beijing.SetSeason(NewWinter())fmt.Println(Beijing)
}
(六)运行结果
=== RUN TestStrategy
北京四季分明,北京的春天,干燥多风;
北京四季分明,北京的夏天,高温多雨;
北京四季分明,北京的秋天,凉爽舒适;
北京四季分明,北京的冬天,干燥寒冷;
--- PASS: TestStrategy (0.00s)
PASS

模板方法模式
(一)概念
模板方法模式是一种行为设计模式,它在超类中定义了一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤。
由于GO语言没有继承的语法,模板方法又是依赖继承实现的设计模式,因此GO语言实现模板方法比较困难, GO语言支持隐式内嵌字段“继承”其他结构体的字段与方法,但是这个并不是真正意义上的继承语法,外层结构重写隐式字段中的算法特定步骤后,无法动态绑定到“继承”过来的算法的框架方法调用中,因此不能实现模板方法模式的语义。
(二)示例
本示例给出一种间接实现模板方法的方式,也比较符合模板方法模式的定义:
- 将多个算法特定步骤组合成一个接口;
- 基类隐式内嵌算法步骤接口,同时调用算法步骤接口的各方法,实现算法的模板方法,此时基类内嵌的算法步骤接口并没有真正的处理行为;
- 子类隐式内嵌基类,并覆写算法步骤接口的方法;
- 通过工厂方法创建具体子类,并将自己的引用赋值给基类中算法步骤接口字段;
以演员装扮为例,演员的装扮是分为化妆,穿衣,配饰三步骤,三个步骤又根据不同角色的演员有所差别,因此演员基类实现装扮的模板方法,对于化妆,穿衣,配饰的三个步骤,在子类演员中具体实现,子类具体演员分为,男演员、女演员和儿童演员;
(三)演员基类
package templatemethodimport ("bytes""fmt"
)// IActor 演员接口
type IActor interface {DressUp() string // 装扮
}// dressBehavior 装扮的多个行为,这里多个行为是私有的,通过DressUp模版方法调用
type dressBehavior interface {makeUp() string // 化妆clothe() string // 穿衣wear() string // 配饰
}// BaseActor 演员基类
type BaseActor struct {roleName string // 扮演角色dressBehavior // 装扮行为
}// DressUp 统一实现演员接口的DressUp模版方法,装扮过程通过不同装扮行为进行扩展
func (b *BaseActor) DressUp() string {buf := bytes.Buffer{}buf.WriteString(fmt.Sprintf("扮演%s的", b.roleName))buf.WriteString(b.makeUp())buf.WriteString(b.clothe())buf.WriteString(b.wear())return buf.String()
}
(四)具体演员
package templatemethod// womanActor 扩展装扮行为的女演员
type womanActor struct {BaseActor
}// NewWomanActor 指定角色创建女演员
func NewWomanActor(roleName string) *womanActor {actor := new(womanActor) // 创建女演员actor.roleName = roleName // 设置角色actor.dressBehavior = actor // 将女演员实现的扩展装扮行为,设置给自己的装扮行为接口return actor
}// 化妆
func (w *womanActor) makeUp() string {return "女演员涂着口红,画着眉毛;"
}// 穿衣
func (w *womanActor) clothe() string {return "穿着连衣裙;"
}// 配饰
func (w *womanActor) wear() string {return "带着耳环,手拎着包;"
}// manActor 扩展装扮行为的男演员
type manActor struct {BaseActor
}func NewManActor(roleName string) *manActor {actor := new(manActor)actor.roleName = roleNameactor.dressBehavior = actor // 将男演员实现的扩展装扮行为,设置给自己的装扮行为接口return actor
}func (m *manActor) makeUp() string {return "男演员刮净胡子,抹上发胶;"
}func (m *manActor) clothe() string {return "穿着一身西装;"
}func (m *manActor) wear() string {return "带上手表,抽着烟;"
}// NewChildActor 扩展装扮行为的儿童演员
type childActor struct {BaseActor
}func NewChildActor(roleName string) *childActor {actor := new(childActor)actor.roleName = roleNameactor.dressBehavior = actor // 将儿童演员实现的扩展装扮行为,设置给自己的装扮行为接口return actor
}func (c *childActor) makeUp() string {return "儿童演员抹上红脸蛋;"
}func (c *childActor) clothe() string {return "穿着一身童装;"
}func (c *childActor) wear() string {return "手里拿着一串糖葫芦;"
}
(五)测试程序
package templatemethodimport ("fmt""testing"
)func TestTemplateMethod(t *testing.T) {showActors(NewWomanActor("妈妈"), NewManActor("爸爸"), NewChildActor("儿子"))
}// showActors 显示演员的装扮信息
func showActors(actors ...IActor) {for _, actor := range actors {fmt.Println(actor.DressUp())}
}
(六)运行结果
=== RUN TestTemplateMethod
扮演妈妈的女演员涂着口红,画着眉毛;穿着连衣裙;带着耳环,手拎着包;
扮演爸爸的男演员刮净胡子,抹上发胶;穿着一身西装;带上手表,抽着烟;
扮演儿子的儿童演员抹上红脸蛋;穿着一身童装;手里拿着一串糖葫芦;
--- PASS: TestTemplateMethod (0.00s)
PASS

访问者模式
(一)概念
访问者模式是一种行为设计模式,它能将算法与其所作用的对象隔离开来。允许你在不修改已有代码的情况下向已有类层次结构中增加新的行为。
访问者接口需要根据被访问者具体类,定义多个相似的访问方法,每个具体类对应一个访问方法;每个被访问者需要实现一个接受访问者对象的方法,方法的实现就是去调用访问者接口对应该类的访问方法;这个接受方法可以传入不同目的访问者接口的具体实现,从而在不修改被访问对象的前提下,增加新的功能;
(二)示例
公司中存在多种类型的员工,包括产品经理、软件工程师、人力资源等,他们的KPI指标不尽相同,产品经理为上线产品数量及满意度,软件工程师为实现的需求数及修改bug数,人力资源为招聘员工的数量;公司要根据员工完成的KPI进行表彰公示,同时根据KPI完成情况定薪酬,这些功能都是员工类职责之外的,不能修改员工本身的类,我们通过访问者模式,实现KPI表彰排名及薪酬发放;
(三)员工结构
package visitorimport "fmt"// Employee 员工接口
type Employee interface {KPI() string // 完成kpi信息Accept(visitor EmployeeVisitor) // 接受访问者对象
}// productManager 产品经理
type productManager struct {name string // 名称productNum int // 上线产品数satisfaction int // 平均满意度
}func NewProductManager(name string, productNum int, satisfaction int) *productManager {return &productManager{name: name,productNum: productNum,satisfaction: satisfaction,}
}func (p *productManager) KPI() string {return fmt.Sprintf("产品经理%s,上线%d个产品,平均满意度为%d", p.name, p.productNum, p.satisfaction)
}func (p *productManager) Accept(visitor EmployeeVisitor) {visitor.VisitProductManager(p)
}// softwareEngineer 软件工程师
type softwareEngineer struct {name string // 姓名requirementNum int // 完成需求数bugNum int // 修复问题数
}func NewSoftwareEngineer(name string, requirementNum int, bugNum int) *softwareEngineer {return &softwareEngineer{name: name,requirementNum: requirementNum,bugNum: bugNum,}
}func (s *softwareEngineer) KPI() string {return fmt.Sprintf("软件工程师%s,完成%d个需求,修复%d个问题", s.name, s.requirementNum, s.bugNum)
}func (s *softwareEngineer) Accept(visitor EmployeeVisitor) {visitor.VisitSoftwareEngineer(s)
}// hr 人力资源
type hr struct {name string // 姓名recruitNum int // 招聘人数
}func NewHR(name string, recruitNum int) *hr {return &hr{name: name,recruitNum: recruitNum,}
}func (h *hr) KPI() string {return fmt.Sprintf("人力资源%s,招聘%d名员工", h.name, h.recruitNum)
}func (h *hr) Accept(visitor EmployeeVisitor) {visitor.VisitHR(h)
}
(四)员工访问者
package visitorimport ("fmt""sort"
)// EmployeeVisitor 员工访问者接口
type EmployeeVisitor interface {VisitProductManager(pm *productManager) // 访问产品经理VisitSoftwareEngineer(se *softwareEngineer) // 访问软件工程师VisitHR(hr *hr) // 访问人力资源
}// kpi kpi对象
type kpi struct {name string // 完成kpi姓名sum int // 完成kpi总数量
}// kpiTopVisitor 员工kpi排名访问者
type kpiTopVisitor struct {top []*kpi
}func (k *kpiTopVisitor) VisitProductManager(pm *productManager) {k.top = append(k.top, &kpi{name: pm.name,sum: pm.productNum + pm.satisfaction,})
}func (k *kpiTopVisitor) VisitSoftwareEngineer(se *softwareEngineer) {k.top = append(k.top, &kpi{name: se.name,sum: se.requirementNum + se.bugNum,})
}func (k *kpiTopVisitor) VisitHR(hr *hr) {k.top = append(k.top, &kpi{name: hr.name,sum: hr.recruitNum,})
}// Publish 发布KPI排行榜
func (k *kpiTopVisitor) Publish() {sort.Slice(k.top, func(i, j int) bool {return k.top[i].sum > k.top[j].sum})for i, curKPI := range k.top {fmt.Printf("第%d名%s:完成KPI总数%d\n", i+1, curKPI.name, curKPI.sum)}
}// salaryVisitor 薪酬访问者
type salaryVisitor struct{}func (s *salaryVisitor) VisitProductManager(pm *productManager) {fmt.Printf("产品经理基本薪资:1000元,KPI单位薪资:100元,")fmt.Printf("%s,总工资为%d元\n", pm.KPI(), (pm.productNum+pm.satisfaction)*100+1000)
}func (s *salaryVisitor) VisitSoftwareEngineer(se *softwareEngineer) {fmt.Printf("软件工程师基本薪资:1500元,KPI单位薪资:80元,")fmt.Printf("%s,总工资为%d元\n", se.KPI(), (se.requirementNum+se.bugNum)*80+1500)
}func (s *salaryVisitor) VisitHR(hr *hr) {fmt.Printf("人力资源基本薪资:800元,KPI单位薪资:120元,")fmt.Printf("%s,总工资为%d元\n", hr.KPI(), hr.recruitNum*120+800)
}
(五)测试程序
package visitorimport "testing"func TestVisitor(t *testing.T) {allEmployees := AllEmployees() // 获取所有员工kpiTop := new(kpiTopVisitor) // 创建KPI排行访问者VisitAllEmployees(kpiTop, allEmployees)kpiTop.Publish() // 发布排行榜salary := new(salaryVisitor) // 创建薪酬访问者VisitAllEmployees(salary, allEmployees)
}// VisitAllEmployees 遍历所有员工调用访问者
func VisitAllEmployees(visitor EmployeeVisitor, allEmployees []Employee) {for _, employee := range allEmployees {employee.Accept(visitor)}
}// AllEmployees 获得所有公司员工
func AllEmployees() []Employee {var employees []Employeeemployees = append(employees, NewHR("小明", 10))employees = append(employees, NewProductManager("小红", 4, 7))employees = append(employees, NewSoftwareEngineer("张三", 10, 5))employees = append(employees, NewSoftwareEngineer("李四", 3, 6))employees = append(employees, NewSoftwareEngineer("王五", 7, 1))return employees
}
(六)运行结果
=== RUN TestVisitor
第1名张三:完成KPI总数15
第2名小红:完成KPI总数11
第3名小明:完成KPI总数10
第4名李四:完成KPI总数9
第5名王五:完成KPI总数8
人力资源基本薪资:800元,KPI单位薪资:120元,人力资源小明,招聘10名员工,总工资为2000元
产品经理基本薪资:1000元,KPI单位薪资:100元,产品经理小红,上线4个产品,平均满意度为7,总工资为2100元
软件工程师基本薪资:1500元,KPI单位薪资:80元,软件工程师张三,完成10个需求,修复5个问题,总工资为2700元
软件工程师基本薪资:1500元,KPI单位薪资:80元,软件工程师李四,完成3个需求,修复6个问题,总工资为2220元
软件工程师基本薪资:1500元,KPI单位薪资:80元,软件工程师王五,完成7个需求,修复1个问题,总工资为2140元
--- PASS: TestVisitor (0.00s)
相关文章:

【转存】Go语言设计模式
导语| 设计模式是针对软件设计中常见问题的工具箱,其中的工具就是各种经过实践验证的解决方案。即使你从未遇到过这些问题,了解模式仍然非常有用,因为它能指导你如何使用面向对象的设计原则来解决各种问题,提高开发效率࿰…...

第十一章 升级与定制
第十一章 升级与定制 一、 RPM 包安装操作 RPM(Redhat Packet Manager)。 ①安装 rpm –i rpm 文件名 (注:⑴常见用法:-ivh 参数显示安装过程和 hash 符#; ⑵覆盖安装:使用- -force 选项。…...

代码随想录算法训练营第二十二天|235. 二叉搜索树的最近公共祖先、701.二叉搜索树中的插入操作、450.删除二叉搜索树中的节点
目录 235. 二叉搜索树的最近公共祖先 1、递归实现 2、迭代法实现 701.二叉搜索树中的插入操作(递归实现) 450.删除二叉搜索树中的节点(递归实现) 235. 二叉搜索树的最近公共祖先 相对于 二叉树的最近公共祖先 本题就简单一些了…...

hbase表出现RIT删除方案
1.删除zookeeper中对应表注册信息 cd /opt/cloudera/parcels/CDH/lib/zookeeper/bin ./zkCli.sh -server node2:2181 --node2为仿真节点,生产需改 deleteall /hbase/table/表名 2.删除hdfs对应表数据 hadoop dfs -rm -r /hbase/data/default/表名 3.删除hbase:met…...

SQL学习(3)
SELECT 语句用于从表中选取数据。 SELECT 列名称 FROM 表名称 SELECT * FROM 表名称关键词 DISTINCT 用于返回唯一不同的值 SELECT DISTINCT 列名称 FROM 表名称WHERE 子句用于规定选择的标准 如需有条件地从表中选取数据,可将 WHERE 子句添加到 SELECT 语句。 S…...

连接型CRM助力医疗企业把“成本中心”变成“利润中心”
在市场竞争日益加剧的情形下,企业获客成本大幅上涨,存量客户的维护和开发开始被重视,售后服务部门的职责在企业中发挥的价值越来越大。因为企业售后服务不仅能帮助客户解决问题的部门,还是客户与企业沟通的桥梁,将客户…...

《Vue.js 设计与实现》—— 03 Vue.js 3 的设计思路
1. 声明式地描述 UI Vue.js 3 是一个声明式的 UI 框架,即用户在使用 Vue.js 3 开发页面时是声明式地描述 UI 的。 编写前端页面涉及的内容如下: DOM 元素:例如是 div 标签还是 a 标签属性:如 a 标签的 href 属性,再…...

2023年湖北省建设厅特种作业操作证报名条件是什么?
建筑施工特种作业人员是指在房屋建筑和市政工程施工活动中,从事可能对本人、他人及周围设备设施的安全造成重大危害作业的人员。建筑施工特种作业人员必须经建设主管部门考核合格,取得建筑施工特种作业人员操作资格证书(以下简称“资格证书”…...

Redis 进阶
🥲 🥸 🤌 🫀 🫁 🥷 🐻❄️🦤 🪶 🦭 🪲 🪳 🪰 🪱 🪴 🫐 🫒 🫑…...

伙伴匹配系统笔记---02
Java 8特性 1. stream / parallelStream 流失处理 2. Optional 可选类 一. 前端整合路由 1. 路由:vue 路由组件库地址:安装 | Vue Router (vuejs.org) 安装:yarn add vue-router@4 2. 整合路由: // 1. 定义路由组件. // 也可以从其他文件导入 const Home = { templ…...

Redis学习——单机版安装
目录 1.解压 2.安装gcc 3.执行make命令 4.复制redis的配置文件到默认安装目录下 5.修改redis.conf文件 6.启动redis服务与客户端 7.查看redis进行是否启动 8.关闭redis服务 9.redis性能测试 注意:安装redis前要安装jdk。 1.解压 [rootlxm148 install]# t…...

第三十一章 React中路由组件和一般组件
在React中,组件是应用程序的构建块。它们是可重用的,可以用于创建复杂的UI。React中有两种类型的组件:路由组件和一般组件。 一般组件 一般组件是React应用程序的基本构建块。它们是可重用的,可以用于创建复杂的UI。它们不知道U…...

怎么把pdf中的某一页分出来?
怎么把pdf中的某一页分出来?PDF格式的文档在日常生活中是非常常见的,相信大家都对其有所了解,并且经常使用。它的主要特点是不允许用户随意编辑其中的内容,当我们仅需要阅读时,PDF文档无疑是十分方便的,尤其…...

MongoDB 聚合操作Map-Reduce
这此之前已经对MongoDB中的一些聚合操作进行了详细的介绍,主要介绍了聚合方法和聚合管道;如果您想对聚合方法和聚合管道进行了解,可以参考: MongoDB 数据库操作汇总https://blog.csdn.net/m1729339749/article/details/130086022…...

shiro CVE-2016-4437 漏洞复现
shiro Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序漏洞原理 在Apache shiro的框架中,执行身份验证时提供了…...

Seqkit-2.2.0 移植指南(openEuler 20.03 LTS SP3)
1.软件介绍 seqkit是一种跨平台的、极快的,全面的fasta/q处理工具。seqkit为所有的主流操作系统提供了一种可执行的双元文件,包括Windows,Linux,MacOS X,并且不依赖于任何的配置或预先配置就可以直接使用。 关于seqk…...

Java版本企业电子招投标采购系统源码——功能模块功能描述+数字化采购管理 采购招投标
功能模块: 待办消息,招标公告,中标公告,信息发布 描述: 全过程数字化采购管理,打造从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通供应商门户具备内外协同的能力,为外部供…...

二十三种设计模式第五篇--原型模式
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建…...

阿里云镜像区别公共镜像、自定义、共享、云市场和社区镜像介绍
阿里云服务器镜像根据来源不同分为公共镜像、自定义镜像、共享镜像、云市场镜像和社区镜像,一般没有特殊情况选择公共镜像,公共镜像是阿里云官网提供的正版授权操作系统,云市场镜像是在纯净版操作系统的基础上预装了相关软件及运行环境&#…...

非线性方程二分法
非线性方程二分法 优点:算法直观、简单、总能保证收敛;局限:收敛速度慢、一般不单独用它求根,仅为了获取根的粗略近似 文章目录 非线性方程二分法[toc]1 二分法基本思想2 二分法实现 1 二分法基本思想 设 f ( x ) f(x) f(x)在 [ …...

H3C防火墙单机旁路部署(网关在防火墙)
防火墙旁路部署在核心交换机上,内网有三个网段vlan 10:172.16.10.1/24、vlan 20:172.16.20.1/24、vlan30:172.16.30.1。要求内网网关在防火墙设备上,由防火墙作为DHCP服务器给终端下发地址,同时由防火墙来控…...

基于密度的无线传感器网络聚类算法的博弈分析(Matlab代码实现)
目录 💥1 概述 📚2 运行结果 🎉3 参考文献 👨💻4 Matlab代码 💥1 概述 提高能源效率是无线传感器网络面临的关键挑战之一,无线传感器网络日益普遍。由于节点(传感器ÿ…...

宕机了?!DolphinScheduler 高可用和 Failover 机制关键时刻保命
高可用性是 Apache DolphinScheduler 的特性之一。它通过冗余来避免单点问题,所有组件天然支持横向扩容;但仅仅保证了冗余还不够,当系统中有节点宕机时,还需要有故障转移机制能够自动将宕机节点正在处理的工作转移到新节点上执行&…...

try(){}用法try-with-resources、try-catch-finally
属于Java7的新特性。 经常会用try-catch来捕获有可能抛出异常的代码。如果其中还涉及到资源的使用的话,最后在finally块中显示的释放掉有可能被占用的资源。 但是如果资源类已经实现了AutoCloseable这个接口的话,可以在try()括号中可以写操作资源的语句(…...

常见Http错误码学习
常见 http 错误码 服务器巡检时比较常见的 http 错误码 400 Bad Request408 Request Timeout499 client has closed connection502 Bad Gateway504 Gateway Timeout 这些错误码反映了服务器什么样的状态,仅看字面意思还不太容易理解,就动手做个试验…...

qemu-基础篇——ARM 链接过程分析(六)
文章目录 ARM 链接过程分析源文件global_bss_file.cglobal_data_fle.cglobal_function_file.cglobal_rodata_file.cmain.c 链接文件 link.lds编译命令及反汇编命令解析 .o 文件global_bss_file.oglobal_data_fle.oglobal_function_file.oglobal_rodata_file.omain.o 链接观察链…...

Java企业工程项目管理系统+spring cloud 系统管理+java 系统设置+二次开发
工程项目各模块及其功能点清单 一、系统管理 1、数据字典:实现对数据字典标签的增删改查操作 2、编码管理:实现对系统编码的增删改查操作 3、用户管理:管理和查看用户角色 4、菜单管理:实现对系统菜单的增删改查操…...

Eureka与Zookeeper的区别
著名的CAP 理论指出,一个分布式系统不可能同时满足 C( 一致性 ) 、 A( 可用性 ) 和 P( 分区容错性 ) 。 由于分区容错性在是分布式系统中必须要保证的,因此我们只能在 A 和 C 之间进行权衡,在此 Zookeeper 保证的是 CP, 而 Eureka 则是 AP…...

顺序表和链表的各种代码实现
一、线性表 在日常生活中,线性表的例子比比皆是。例如,26个英文字母的字母表(A,B,C,……,Z)是一个线性表,表中的数据元素式单个字母。在稍复杂的线性表中,一个数据元素可以包含若干个数据项。例…...

C# 介绍三种不同组件创建PDF文档的方式
1 c# 数据保存为PDF(一) (spire pdf篇) 2 c# 数据保存为PDF(二) (Aspose pdf篇) 3 c# 数据保存为PDF(三) (PdfSharp篇) 组件名称 绘制…...