0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

實(shí)踐GoF的23種設(shè)計(jì)模式:命令模式簡介

元閏子的邀請 ? 來源:元閏子的邀請 ? 2023-01-13 16:36 ? 次閱讀

簡介

現(xiàn)在的軟件系統(tǒng)往往是分層設(shè)計(jì)。在業(yè)務(wù)層執(zhí)行一次請求時(shí),我們很清楚請求的上下文,包括,請求是做什么的、參數(shù)有哪些、請求的接收者是誰、返回值是怎樣的。相反,基礎(chǔ)設(shè)施層并不需要完全清楚業(yè)務(wù)上下文,它只需知道請求的接收者是誰即可,否則就耦合過深了。

因此,我們需要對(duì)請求進(jìn)行抽象,將上下文信息封裝到請求對(duì)象里,這其實(shí)就是命令模式,而該請求對(duì)象就是 Command。

GoF 對(duì)命令模式(Command Pattern)的定義如下:

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

也即,命令模式可將請求轉(zhuǎn)換為一個(gè)包含與請求相關(guān)的所有信息的對(duì)象, 它能將請求參數(shù)化、延遲執(zhí)行、實(shí)現(xiàn) Undo / Redo 操作等。

上述的請求是廣義上的概念,可以是網(wǎng)絡(luò)請求,也可以是函數(shù)調(diào)用,更通用地,指一個(gè)動(dòng)作。

命令模式主要包含 3 種角色:

Command,命令,是對(duì)請求的抽象。具體的命令實(shí)現(xiàn)時(shí),通常會(huì)引用 Receiver。

Invoker,請求的發(fā)起發(fā)起方,它并不清楚 Command 和 Receiver 的實(shí)現(xiàn)細(xì)節(jié),只管調(diào)用命令的接口。

Receiver,請求的接收方。

9f451862-82a7-11ed-8abf-dac502259ad0.jpg

命令模式,一方面,能夠使得 Invoker 與 Receiver 消除彼此之間的耦合,讓對(duì)象之間的調(diào)用關(guān)系更加靈活;另一方面,能夠很方便地實(shí)現(xiàn)延遲執(zhí)行、Undo、Redo 等操作,因此被廣泛應(yīng)用在軟件設(shè)計(jì)中。

UML 結(jié)構(gòu)

9ff447b0-82a7-11ed-8abf-dac502259ad0.jpg

場景上下文

在簡單的分布式應(yīng)用系統(tǒng)(示例代碼工程)中,db 模塊用來存儲(chǔ)服務(wù)注冊信息和系統(tǒng)監(jiān)控?cái)?shù)據(jù)。

其中,服務(wù)注冊信息拆成了profiles和regions兩個(gè)表,在服務(wù)發(fā)現(xiàn)的業(yè)務(wù)邏輯中,通常需要同時(shí)操作兩個(gè)表,為了避免兩個(gè)表數(shù)據(jù)不一致的問題,db 模塊需要提供事務(wù)功能:

a01d9228-82a7-11ed-8abf-dac502259ad0.jpg

事務(wù)的核心功能之一是,當(dāng)其中某個(gè)語句執(zhí)行失敗時(shí),之前已執(zhí)行成功的語句能夠回滾,而使用命令模式能夠很方便地實(shí)現(xiàn)該功能。

代碼實(shí)現(xiàn)

//demo/db/transaction.go
packagedb

//Command執(zhí)行數(shù)據(jù)庫操作的命令接口
//關(guān)鍵點(diǎn)1:定義命令抽象接口
typeCommandinterface{
//關(guān)鍵點(diǎn)2:命令抽象接口中聲明執(zhí)行命令的方法
Exec()error//Exec執(zhí)行insert、update、delete命令
//關(guān)鍵點(diǎn)3:如果有撤銷功能,則需要定義Undo方法
Undo()//Undo回滾命令
setDb(dbDb)//SetDb設(shè)置關(guān)聯(lián)的數(shù)據(jù)庫
}

//TransactionDb事務(wù)實(shí)現(xiàn),事務(wù)接口的調(diào)用順序?yàn)閎egin->exec->exec>...->commit
//關(guān)鍵點(diǎn)4:定義Invoker對(duì)象
typeTransactionstruct{
namestring
dbDb
//關(guān)鍵點(diǎn)5:Invoker對(duì)象持有Command的引用
cmds[]Command
}
//Begin開啟一個(gè)事務(wù)
func(t*Transaction)Begin(){
t.cmds=make([]Command,0)
}
//Exec在事務(wù)中執(zhí)行命令,先緩存到cmds隊(duì)列中,等commit時(shí)再執(zhí)行
func(t*Transaction)Exec(cmdCommand)error{
ift.cmds==nil{
returnErrTransactionNotBegin
}
cmd.setDb(t.db)
t.cmds=append(t.cmds,cmd)
returnnil
}
//Commit提交事務(wù),執(zhí)行隊(duì)列中的命令,如果有命令失敗,則回滾后返回錯(cuò)誤
//關(guān)鍵點(diǎn)6:為Invoker對(duì)象定義Call方法,在方法內(nèi)調(diào)用Command的執(zhí)行方法Exec
func(t*Transaction)Commit()error{
history:=&cmdHistory{history:make([]Command,0,len(t.cmds))}
for_,cmd:=ranget.cmds{
iferr:=cmd.Exec();err!=nil{
history.rollback()
returnerr
}
history.add(cmd)
}
returnnil
}
//cmdHistory命令執(zhí)行歷史
typecmdHistorystruct{
history[]Command
}
func(c*cmdHistory)add(cmdCommand){
c.history=append(c.history,cmd)
}
//關(guān)鍵點(diǎn)7:在回滾方法中,調(diào)用已執(zhí)行命令的Undo方法
func(c*cmdHistory)rollback(){
fori:=len(c.history)-1;i>=0;i--{
c.history[i].Undo()
}
}

//InsertCmd插入命令
//關(guān)鍵點(diǎn)8:定義具體的命令類,實(shí)現(xiàn)Command接口
typeInsertCmdstruct{
//關(guān)鍵點(diǎn)9:命令通常持有接收者的引用,以便在執(zhí)行方法中與接收者交互
dbDb
tableNamestring
primaryKeyinterface{}
newRecordinterface{}
}
//關(guān)鍵點(diǎn)10:命令對(duì)象執(zhí)行方法中,調(diào)用Receiver的Action方法,這里的Receiver為db對(duì)象,Action方法為Insert方法
func(i*InsertCmd)Exec()error{
returni.db.Insert(i.tableName,i.primaryKey,i.newRecord)
}
func(i*InsertCmd)Undo(){
i.db.Delete(i.tableName,i.primaryKey)
}
func(i*InsertCmd)setDb(dbDb){
i.db=db
}

//UpdateCmd更新命令
typeUpdateCmdstruct{...}
//DeleteCmd刪除命令
typeDeleteCmdstruct{...}

客戶端可以這么使用:

funcclient(){
transaction:=db.CreateTransaction("register"+profile.Id)
transaction.Begin()
rcmd:=db.NewUpdateCmd(regionTable).WithPrimaryKey(profile.Region.Id).WithRecord(profile.Region)
transaction.Exec(rcmd)
pcmd:=db.NewUpdateCmd(profileTable).WithPrimaryKey(profile.Id).WithRecord(profile.ToTableRecord())
transaction.Exec(pcmd)
iferr:=transaction.Commit();err!=nil{
return...
}
return...
}

總結(jié)實(shí)現(xiàn)命令模式的幾個(gè)關(guān)鍵點(diǎn):

定義命令抽象接口,本例子中為Command接口。

在命令抽象接口中聲明執(zhí)行命令的方法,本例子中為Exec方法。

如果要實(shí)現(xiàn)撤銷功能,還需要為命令對(duì)象定義Undo方法,在操作回滾時(shí)調(diào)用。

定義Invoker對(duì)象,本例子中為Transaction對(duì)象。

在 Invoker 對(duì)象持有 Command 的引用,本例子為Command的切片cmds。

為 Invoker 對(duì)象定義 Call 方法,用于執(zhí)行具體的命令,在方法內(nèi)調(diào)用 Command 的執(zhí)行方法 ,本例子中為Transaction.Commit方法。

如果要實(shí)現(xiàn)撤銷功能,還要在回滾方法中,調(diào)用已執(zhí)行命令的Undo方法,本例子中為cmdHistory.rollback方法。

定義具體的命令類,實(shí)現(xiàn)Command接口,本例子中為InsertCmd、UpdateCmd、DeleteCmd。

命令通常持有接收者的引用,以便在執(zhí)行方法中與接收者交互。本例子中,Receiver 為Db對(duì)象。

最后,在命令對(duì)象執(zhí)行方法中,調(diào)用 Receiver 的 Action 方法,本例子中, Receiver 的 Action 方法為db.Insert方法。

值得注意的是,本例子中Transaction對(duì)象在Transaction.Exec方法中只是將Command保存在隊(duì)列中,只有當(dāng)調(diào)用Transaction.Commit方法時(shí)才延遲執(zhí)行相應(yīng)的命令。

擴(kuò)展

os/exec中的命令模式

Go 標(biāo)準(zhǔn)庫的os/exec包也用到了命令模式。

packagemain

import(
"os/exec"
)

//對(duì)應(yīng)命令模式中的Invoker
funcmain(){
cmd:=exec.Command("sleep","1")
err:=cmd.Run()
}

在上述例子中,我們通過exec.Command方法將一個(gè) shell 命令轉(zhuǎn)換成一個(gè)命令對(duì)象exec.Cmd,其中的Cmd.Run()方法即是命令執(zhí)行方法;而main()函數(shù),對(duì)應(yīng)到命令模式中的 Invoker;Receiver 則是操作系統(tǒng)執(zhí)行 shell 命令的具體進(jìn)程,從exec.Cmd的源碼中可以看到:

//src/os/exec/exec.go
packageexec

//對(duì)應(yīng)命令模式中的Command
typeCmdstruct{
...
//對(duì)應(yīng)命令模式中的Receiver
Process*os.Process
...
}

//對(duì)應(yīng)命令模式中Command的執(zhí)行方法
func(c*Cmd)Run()error{
iferr:=c.Start();err!=nil{
returnerr
}
returnc.Wait()
}

func(c*Cmd)Start()error{
...
//Command與Receiver的交互
c.Process,err=os.StartProcess(c.Path,c.argv(),&os.ProcAttr{...})
...
}
a046042e-82a7-11ed-8abf-dac502259ad0.jpg

CQRS 架構(gòu)

CQRS 架構(gòu),全稱為 Command Query Responsibility Segregation,命令查詢職責(zé)隔離架構(gòu)。

CQRS 架構(gòu)是微服務(wù)架構(gòu)模式中的一種,它利用事件(命令)來維護(hù)從多個(gè)服務(wù)復(fù)制數(shù)據(jù)的只讀視圖,通過讀寫分離思想,提升微服務(wù)架構(gòu)下查詢的性能。

a06767e0-82a7-11ed-8abf-dac502259ad0.jpg

CQRS 架構(gòu)可分為命令端查詢端,其中命令端負(fù)責(zé)數(shù)據(jù)的更新;查詢端負(fù)責(zé)數(shù)據(jù)的查詢。命令端的寫數(shù)據(jù)庫在數(shù)據(jù)更新時(shí),會(huì)向查詢端的只讀數(shù)據(jù)庫發(fā)送一個(gè)同步數(shù)據(jù)的事件,保證數(shù)據(jù)的最終一致性。

其中的命令端,就使用到了命令模式的思想,將數(shù)據(jù)更新請求封裝成命令,異步更新到寫數(shù)據(jù)庫中。

典型應(yīng)用場景

事務(wù)模式。事務(wù)模式下往往需要 Undo 操作,使用命令模式實(shí)現(xiàn)起來很方便。

遠(yuǎn)程執(zhí)行。Go 標(biāo)準(zhǔn)庫下的exec.Cmd、http.Client都屬于該類型,將請求封裝成命令來執(zhí)行。

CQRS 架構(gòu)。微服務(wù)架構(gòu)模式中的一種,通過命令模式來實(shí)現(xiàn)數(shù)據(jù)的異步更新。

延遲執(zhí)行。當(dāng)你希望一個(gè)操作能夠延遲執(zhí)行時(shí),通常會(huì)將它封裝成命令,然后放到一個(gè)隊(duì)列中。

優(yōu)缺點(diǎn)

優(yōu)點(diǎn)

符合單一職責(zé)原則。在命令模式下,每個(gè)命令都是職責(zé)單一、松耦合的;當(dāng)然也可以通過組合的方式,將多個(gè)簡單的命令組合成一個(gè)負(fù)責(zé)的命令。

可以很方便地實(shí)現(xiàn)操作的延遲執(zhí)行、回滾、重做等。

在分布式架構(gòu)下,命令模式能夠方便地實(shí)現(xiàn)異步的數(shù)據(jù)更新、方法調(diào)用等,提升性能。

缺點(diǎn)

命令模式下,調(diào)用往往是異步的,而異步會(huì)導(dǎo)致系統(tǒng)變得復(fù)雜,問題出現(xiàn)時(shí)不好定位解決。

隨著業(yè)務(wù)越來越復(fù)雜,命令對(duì)象也會(huì)增多,代碼會(huì)變得更難維護(hù)。

與其他模式的關(guān)聯(lián)

在實(shí)現(xiàn) Undo/Redo 操作時(shí),你通常需要同時(shí)使用 命令模式 和備忘錄模式

另外,命令模式 也常常和觀察者模式一起出現(xiàn),比如在 CQRS 架構(gòu)中,當(dāng)命令端更新數(shù)據(jù)庫后,寫數(shù)據(jù)庫就會(huì)通過事件將數(shù)據(jù)同步到讀數(shù)據(jù)庫上,這里就用到了觀察者模式。




審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • REGOFF
    +關(guān)注

    關(guān)注

    0

    文章

    2

    瀏覽量

    2143

原文標(biāo)題:【Go實(shí)現(xiàn)】實(shí)踐GoF的23種設(shè)計(jì)模式:命令模式

文章出處:【微信號(hào):yuanrunzi,微信公眾號(hào):元閏子的邀請】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    23基本的設(shè)計(jì)模式總結(jié)

    一樣。?提到設(shè)計(jì)模式,不得不感謝GoF(***,四人組),他們1995年出版的《設(shè)計(jì)模式》一書,第一次將設(shè)計(jì)模式提升到理論高度,并將之規(guī)范化。書中一共總結(jié)了
    發(fā)表于 03-01 06:08

    Command模式與動(dòng)態(tài)語言

    Gof的設(shè)計(jì)模式中,有一個(gè)模式引起的爭議比較大,有很多人甚至認(rèn)為這個(gè)模式應(yīng)該排除在OO模式之外,原因在于它不具有OO的特性
    發(fā)表于 06-22 10:20 ?982次閱讀
    Command<b class='flag-5'>模式</b>與動(dòng)態(tài)語言

    C#23設(shè)計(jì)模式【完整】

    C#23設(shè)計(jì)模式
    發(fā)表于 08-21 17:38 ?71次下載

    Java的23設(shè)計(jì)模式詳細(xì)資料說明

    Java的23設(shè)計(jì)模式詳細(xì)資料說明。
    發(fā)表于 12-13 16:28 ?11次下載
    Java的<b class='flag-5'>23</b><b class='flag-5'>種</b>設(shè)計(jì)<b class='flag-5'>模式</b>詳細(xì)資料說明

    思科網(wǎng)絡(luò)交換機(jī)的6命令配置模式

    我們在配置交換機(jī)的時(shí)候首先要了解的就是交換機(jī)命令模式,小編用Cisco思科交換機(jī)為例帶大家了解交換機(jī)的6配置模式
    的頭像 發(fā)表于 04-13 16:41 ?1.3w次閱讀

    23java設(shè)計(jì)模式

    JAVA的設(shè)計(jì)模式經(jīng)前人總結(jié)可以分為23 設(shè)計(jì)模式根據(jù)使用類型可以分為三: 1、創(chuàng)建模式
    發(fā)表于 09-23 15:17 ?1次下載

    GoF設(shè)計(jì)模式之訪問者模式

    訪問者模式的目的是,解耦數(shù)據(jù)結(jié)構(gòu)和算法,使得系統(tǒng)能夠在不改變現(xiàn)有代碼結(jié)構(gòu)的基礎(chǔ)上,為對(duì)象新增一新的操作。
    的頭像 發(fā)表于 10-08 11:05 ?658次閱讀

    設(shè)計(jì)模式最佳實(shí)踐探索—策略模式

    根據(jù)不同的應(yīng)用場景與意圖,設(shè)計(jì)模式主要分為創(chuàng)建型模式、結(jié)構(gòu)型模式和行為型模式三類。本文主要探索行為型模式中的策略
    的頭像 發(fā)表于 10-31 14:24 ?911次閱讀

    實(shí)踐GoF23設(shè)計(jì)模式:備忘錄模式

    相對(duì)于代理模式、工廠模式等設(shè)計(jì)模式,備忘錄模式(Memento)在我們?nèi)粘i_發(fā)中出鏡率并不高,除了應(yīng)用場景的限制之外,另一個(gè)原因,可能是備忘錄模式
    的頭像 發(fā)表于 11-25 09:05 ?506次閱讀
    <b class='flag-5'>實(shí)踐</b><b class='flag-5'>GoF</b>的<b class='flag-5'>23</b><b class='flag-5'>種</b>設(shè)計(jì)<b class='flag-5'>模式</b>:備忘錄<b class='flag-5'>模式</b>

    linux中vim三模式切換

    在Linux中,Vim編輯器具有三模式,分別是命令模式、插入模式和末行模式。這三
    的頭像 發(fā)表于 11-26 15:39 ?2333次閱讀

    實(shí)踐GoF23設(shè)計(jì)模式:適配器模式

    適配器模式所做的就是將一個(gè)接口 Adaptee,通過適配器 Adapter 轉(zhuǎn)換成 Client 所期望的另一個(gè)接口 Target 來使用,實(shí)現(xiàn)原理也很簡單,就是 Adapter 通過實(shí)現(xiàn) Target接口,并在對(duì)應(yīng)的方法中調(diào)用 Adaptee 的接口實(shí)現(xiàn)。
    的頭像 發(fā)表于 12-10 14:00 ?462次閱讀
    <b class='flag-5'>實(shí)踐</b><b class='flag-5'>GoF</b>的<b class='flag-5'>23</b><b class='flag-5'>種</b>設(shè)計(jì)<b class='flag-5'>模式</b>:適配器<b class='flag-5'>模式</b>

    實(shí)踐GoF23設(shè)計(jì)模式:解釋器模式

    解釋器模式(Interpreter Pattern)應(yīng)該是 GoF23 設(shè)計(jì)模式中使用頻率最少的一
    的頭像 發(fā)表于 04-01 11:01 ?606次閱讀
    <b class='flag-5'>實(shí)踐</b><b class='flag-5'>GoF</b>的<b class='flag-5'>23</b><b class='flag-5'>種</b>設(shè)計(jì)<b class='flag-5'>模式</b>:解釋器<b class='flag-5'>模式</b>

    實(shí)踐GoF23設(shè)計(jì)模式實(shí)現(xiàn):橋接模式

    也即,將抽象部分和實(shí)現(xiàn)部分進(jìn)行解耦,使得它們能夠各自往獨(dú)立的方向變化。
    的頭像 發(fā)表于 04-14 09:30 ?398次閱讀
    <b class='flag-5'>實(shí)踐</b><b class='flag-5'>GoF</b>的<b class='flag-5'>23</b><b class='flag-5'>種</b>設(shè)計(jì)<b class='flag-5'>模式</b>實(shí)現(xiàn):橋接<b class='flag-5'>模式</b>

    vim的三工作模式是什么?如何切換

    Vim是一款功能強(qiáng)大的文本編輯器,它具有多種工作模式,以滿足不同用戶的需求。在Vim中,主要有三工作模式:普通模式、插入模式
    的頭像 發(fā)表于 08-30 14:50 ?2003次閱讀

    解決睡眠模式進(jìn)入系統(tǒng)在速度命令模式下的問題

    電子發(fā)燒友網(wǎng)站提供《解決睡眠模式進(jìn)入系統(tǒng)在速度命令模式下的問題.pdf》資料免費(fèi)下載
    發(fā)表于 09-24 10:34 ?0次下載
    解決睡眠<b class='flag-5'>模式</b>進(jìn)入系統(tǒng)在速度<b class='flag-5'>命令</b><b class='flag-5'>模式</b>下的問題