導語| 設計模式是針對軟體設計中常見問題的工具箱,其中的工具就是各種經過實踐驗證的解決方案。即使你從未遇到過這些問題,瞭解模式仍然非常有用,因為它能指導你如何使用物件導向的設計原則來解決各種問題,提高開發效率,降低開發成本;本文囊括了GO語言實現的經典設計模式示例,每個示例都精心設計,力求符合模式結構,可作為日常編碼參考,同時一些常用的設計模式融入了開發實踐經驗總結,幫助大家在平時工作中靈活運用。
責任鏈模式
(一)概念
責任鏈模式是一種行為設計模式, 允許你將請求沿著處理者鏈進行傳送。收到請求後,每個處理者均可對請求進行處理,或將其傳遞給鏈上的下個處理者。
該模式允許多個物件來對請求進行處理,而無需讓傳送者類與具體接收者類相耦合。鏈可在執行時由遵循標準處理者介面的任意處理者動態生成。
一般意義上的責任鏈模式是說,請求在鏈上流轉時任何一個滿足條件的節點處理完請求後就會停止流轉並返回,不過還可以根據不同的業務情況做一些改進:
- 請求可以流經處理鏈的所有節點,不同節點會對請求做不同職責的處理;
- 可以透過上下文引數儲存請求物件及上游節點的處理結果,供下游節點依賴,並進一步處理;
- 處理鏈可支援節點的非同步處理,透過實現特定介面判斷,是否需要非同步處理;
- 責任鏈對於請求處理節點可以設定停止標誌位,不是異常,是一種滿足業務流轉的中斷;
- 責任鏈的拼接方式存在兩種,一種是節點遍歷,一個節點一個節點順序執行;另一種是節點巢狀,內層節點嵌入在外層節點執行邏輯中,類似遞迴,或者“回”行結構;
- 責任鏈的節點巢狀拼接方式多被稱為攔截器鏈或者過濾器鏈,更易於實現業務流程的切面,比如監控業務執行時長,日誌輸出,許可權校驗等;
(二)示例
本示例模擬實現機場登機過程,第一步辦理登機牌,第二步如果有行李,就辦理託運,第三步核實身份,第四步安全檢查,第五步完成登機;其中行李託運是可選的,其他步驟必選,必選步驟有任何不滿足就終止登機;旅客物件作為請求引數上下文,每個步驟會根據旅客物件狀態判斷是否處理或流轉下一個節點;
(三)登機過程
package chainofresponsibility
import "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 = true
fmt.Printf("旅客%s成功登機;\n", passenger.name)
}
(四)測試程式
package chainofresponsibility
import "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 command
import "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 command
import (
"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 iterator
import "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時,返回老師成員,否則遍歷學生slice
if 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 string
teacher *Teacher
students []*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 iterator
import (
"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 mediator
import "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 string
airportMediator 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 bool
waitingQueue []Aircraft // 等待降落的飛機佇列
}
func (a *ApproachTower) CanLandAirport(aircraft Aircraft) bool {
if a.hasFreeAirstrip {
a.hasFreeAirstrip = false
return 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 mediator
import "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 memento
import "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 memento
import "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 memento
import (
"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 observer
import "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 observer
import "fmt"
// MsgType 信用卡訊息型別
type MsgType int
const (
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 += money
c.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 observer
import "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 state
import "fmt"
// BatteryState 電池狀態介面,支援手機充電線插拔事件
type BatteryState interface {
ConnectPlug(iPhone *IPhone) string
DisconnectPlug(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 state
import "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 state
import (
"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 strategy
import "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 strategy
import (
"fmt"
)
// City 城市
type City struct {
name string
feature string
season 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 strategy
import (
"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 templatemethod
import (
"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 = roleName
actor.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 = roleName
actor.dressBehavior = actor // 將兒童演員實現的擴充套件裝扮行為,設定給自己的裝扮行為介面
return actor
}
func (c *childActor) makeUp() string {
return "兒童演員抹上紅臉蛋;"
}
func (c *childActor) clothe() string {
return "穿著一身童裝;"
}
func (c *childActor) wear() string {
return "手裡拿著一串糖葫蘆;"
}
(五)測試程式
package templatemethod
import (
"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 visitor
import "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 visitor
import (
"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 visitor
import "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 []Employee
employees = 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)