簡介
現(xiàn)在的軟件系統(tǒng)往往是分層設(shè)計。在業(yè)務(wù)層執(zhí)行一次請求時,我們很清楚請求的上下文,包括,請求是做什么的、參數(shù)有哪些、請求的接收者是誰、返回值是怎樣的。相反,基礎(chǔ)設(shè)施層并不需要完全清楚業(yè)務(wù)上下文,它只需知道請求的接收者是誰即可,否則就耦合過深了。
因此,我們需要對請求進行抽象,將上下文信息封裝到請求對象里,這其實就是命令模式,而該請求對象就是 Command。
GoF 對命令模式(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)換為一個包含與請求相關(guān)的所有信息的對象, 它能將請求參數(shù)化、延遲執(zhí)行、實現(xiàn) Undo / Redo 操作等。
上述的請求是廣義上的概念,可以是網(wǎng)絡(luò)請求,也可以是函數(shù)調(diào)用,更通用地,指一個動作。
命令模式主要包含 3 種角色:
Command,命令,是對請求的抽象。具體的命令實現(xiàn)時,通常會引用 Receiver。
Invoker,請求的發(fā)起發(fā)起方,它并不清楚 Command 和 Receiver 的實現(xiàn)細節(jié),只管調(diào)用命令的接口。
Receiver,請求的接收方。
命令模式,一方面,能夠使得 Invoker 與 Receiver 消除彼此之間的耦合,讓對象之間的調(diào)用關(guān)系更加靈活;另一方面,能夠很方便地實現(xiàn)延遲執(zhí)行、Undo、Redo 等操作,因此被廣泛應用在軟件設(shè)計中。
UML 結(jié)構(gòu)
場景上下文
在簡單的分布式應用系統(tǒng)(示例代碼工程)中,db 模塊用來存儲服務(wù)注冊信息和系統(tǒng)監(jiān)控數(shù)據(jù)。
其中,服務(wù)注冊信息拆成了profiles和regions兩個表,在服務(wù)發(fā)現(xiàn)的業(yè)務(wù)邏輯中,通常需要同時操作兩個表,為了避免兩個表數(shù)據(jù)不一致的問題,db 模塊需要提供事務(wù)功能:
事務(wù)的核心功能之一是,當其中某個語句執(zhí)行失敗時,之前已執(zhí)行成功的語句能夠回滾,而使用命令模式能夠很方便地實現(xiàn)該功能。
代碼實現(xiàn)
//demo/db/transaction.go packagedb //Command執(zhí)行數(shù)據(jù)庫操作的命令接口 //關(guān)鍵點1:定義命令抽象接口 typeCommandinterface{ //關(guān)鍵點2:命令抽象接口中聲明執(zhí)行命令的方法 Exec()error//Exec執(zhí)行insert、update、delete命令 //關(guān)鍵點3:如果有撤銷功能,則需要定義Undo方法 Undo()//Undo回滾命令 setDb(dbDb)//SetDb設(shè)置關(guān)聯(lián)的數(shù)據(jù)庫 } //TransactionDb事務(wù)實現(xiàn),事務(wù)接口的調(diào)用順序為begin->exec->exec>...->commit //關(guān)鍵點4:定義Invoker對象 typeTransactionstruct{ namestring dbDb //關(guān)鍵點5:Invoker對象持有Command的引用 cmds[]Command } //Begin開啟一個事務(wù) func(t*Transaction)Begin(){ t.cmds=make([]Command,0) } //Exec在事務(wù)中執(zhí)行命令,先緩存到cmds隊列中,等commit時再執(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í)行隊列中的命令,如果有命令失敗,則回滾后返回錯誤 //關(guān)鍵點6:為Invoker對象定義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)鍵點7:在回滾方法中,調(diào)用已執(zhí)行命令的Undo方法 func(c*cmdHistory)rollback(){ fori:=len(c.history)-1;i>=0;i--{ c.history[i].Undo() } } //InsertCmd插入命令 //關(guān)鍵點8:定義具體的命令類,實現(xiàn)Command接口 typeInsertCmdstruct{ //關(guān)鍵點9:命令通常持有接收者的引用,以便在執(zhí)行方法中與接收者交互 dbDb tableNamestring primaryKeyinterface{} newRecordinterface{} } //關(guān)鍵點10:命令對象執(zhí)行方法中,調(diào)用Receiver的Action方法,這里的Receiver為db對象,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é)實現(xiàn)命令模式的幾個關(guān)鍵點:
定義命令抽象接口,本例子中為Command接口。
在命令抽象接口中聲明執(zhí)行命令的方法,本例子中為Exec方法。
如果要實現(xiàn)撤銷功能,還需要為命令對象定義Undo方法,在操作回滾時調(diào)用。
定義Invoker對象,本例子中為Transaction對象。
在 Invoker 對象持有 Command 的引用,本例子為Command的切片cmds。
為 Invoker 對象定義 Call 方法,用于執(zhí)行具體的命令,在方法內(nèi)調(diào)用 Command 的執(zhí)行方法 ,本例子中為Transaction.Commit方法。
如果要實現(xiàn)撤銷功能,還要在回滾方法中,調(diào)用已執(zhí)行命令的Undo方法,本例子中為cmdHistory.rollback方法。
定義具體的命令類,實現(xiàn)Command接口,本例子中為InsertCmd、UpdateCmd、DeleteCmd。
命令通常持有接收者的引用,以便在執(zhí)行方法中與接收者交互。本例子中,Receiver 為Db對象。
最后,在命令對象執(zhí)行方法中,調(diào)用 Receiver 的 Action 方法,本例子中, Receiver 的 Action 方法為db.Insert方法。
值得注意的是,本例子中Transaction對象在Transaction.Exec方法中只是將Command保存在隊列中,只有當調(diào)用Transaction.Commit方法時才延遲執(zhí)行相應的命令。
擴展
os/exec中的命令模式
Go 標準庫的os/exec包也用到了命令模式。
packagemain import( "os/exec" ) //對應命令模式中的Invoker funcmain(){ cmd:=exec.Command("sleep","1") err:=cmd.Run() }
在上述例子中,我們通過exec.Command方法將一個 shell 命令轉(zhuǎn)換成一個命令對象exec.Cmd,其中的Cmd.Run()方法即是命令執(zhí)行方法;而main()函數(shù),對應到命令模式中的 Invoker;Receiver 則是操作系統(tǒng)執(zhí)行 shell 命令的具體進程,從exec.Cmd的源碼中可以看到:
//src/os/exec/exec.go packageexec //對應命令模式中的Command typeCmdstruct{ ... //對應命令模式中的Receiver Process*os.Process ... } //對應命令模式中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{...}) ... }
CQRS 架構(gòu)
CQRS 架構(gòu),全稱為 Command Query Responsibility Segregation,命令查詢職責隔離架構(gòu)。
CQRS 架構(gòu)是微服務(wù)架構(gòu)模式中的一種,它利用事件(命令)來維護從多個服務(wù)復制數(shù)據(jù)的只讀視圖,通過讀寫分離思想,提升微服務(wù)架構(gòu)下查詢的性能。
CQRS 架構(gòu)可分為命令端和查詢端,其中命令端負責數(shù)據(jù)的更新;查詢端負責數(shù)據(jù)的查詢。命令端的寫數(shù)據(jù)庫在數(shù)據(jù)更新時,會向查詢端的只讀數(shù)據(jù)庫發(fā)送一個同步數(shù)據(jù)的事件,保證數(shù)據(jù)的最終一致性。
其中的命令端,就使用到了命令模式的思想,將數(shù)據(jù)更新請求封裝成命令,異步更新到寫數(shù)據(jù)庫中。
典型應用場景
事務(wù)模式。事務(wù)模式下往往需要 Undo 操作,使用命令模式實現(xiàn)起來很方便。
遠程執(zhí)行。Go 標準庫下的exec.Cmd、http.Client都屬于該類型,將請求封裝成命令來執(zhí)行。
CQRS 架構(gòu)。微服務(wù)架構(gòu)模式中的一種,通過命令模式來實現(xiàn)數(shù)據(jù)的異步更新。
延遲執(zhí)行。當你希望一個操作能夠延遲執(zhí)行時,通常會將它封裝成命令,然后放到一個隊列中。
優(yōu)缺點
優(yōu)點
符合單一職責原則。在命令模式下,每個命令都是職責單一、松耦合的;當然也可以通過組合的方式,將多個簡單的命令組合成一個負責的命令。
可以很方便地實現(xiàn)操作的延遲執(zhí)行、回滾、重做等。
在分布式架構(gòu)下,命令模式能夠方便地實現(xiàn)異步的數(shù)據(jù)更新、方法調(diào)用等,提升性能。
缺點
命令模式下,調(diào)用往往是異步的,而異步會導致系統(tǒng)變得復雜,問題出現(xiàn)時不好定位解決。
隨著業(yè)務(wù)越來越復雜,命令對象也會增多,代碼會變得更難維護。
與其他模式的關(guān)聯(lián)
在實現(xiàn) Undo/Redo 操作時,你通常需要同時使用 命令模式 和備忘錄模式。
另外,命令模式 也常常和觀察者模式一起出現(xiàn),比如在 CQRS 架構(gòu)中,當命令端更新數(shù)據(jù)庫后,寫數(shù)據(jù)庫就會通過事件將數(shù)據(jù)同步到讀數(shù)據(jù)庫上,這里就用到了觀察者模式。
審核編輯:劉清
-
REGOFF
+關(guān)注
關(guān)注
0文章
2瀏覽量
2152
原文標題:【Go實現(xiàn)】實踐GoF的23種設(shè)計模式:命令模式
文章出處:【微信號:yuanrunzi,微信公眾號:元閏子的邀請】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論