實踐GoF的23種設計模式:命令模式

華為雲開發者聯盟發表於2022-12-27
摘要:命令模式可將請求轉換為一個包含與請求相關的所有資訊的物件, 它能將請求引數化、延遲執行、實現 Undo / Redo 操作等。

本文分享自華為雲社群《【Go實現】實踐GoF的23種設計模式:命令模式》,作者:元閏子。

簡介

現在的軟體系統往往是分層設計。在業務層執行一次請求時,我們很清楚請求的上下文,包括,請求是做什麼的、引數有哪些、請求的接收者是誰、返回值是怎樣的。相反,基礎設施層並不需要完全清楚業務上下文,它只需知道請求的接收者是誰即可,否則就耦合過深了。

因此,我們需要對請求進行抽象,將上下文資訊封裝到請求物件裡,這其實就是命令模式,而該請求物件就是 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.

也即,命令模式可將請求轉換為一個包含與請求相關的所有資訊的物件, 它能將請求引數化、延遲執行、實現 Undo / Redo 操作等。

上述的請求是廣義上的概念,可以是網路請求,也可以是函式呼叫,更通用地,指一個動作。

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

  1. Command,命令,是對請求的抽象。具體的命令實現時,通常會引用 Receiver。
  2. Invoker,請求的發起發起方,它並不清楚 Command 和 Receiver 的實現細節,只管呼叫命令的介面。
  3. Receiver,請求的接收方。
實踐GoF的23種設計模式:命令模式

命令模式,一方面,能夠使得 Invoker 與 Receiver 消除彼此之間的耦合,讓物件之間的呼叫關係更加靈活;另一方面,能夠很方便地實現延遲執行、Undo、Redo 等操作,因此被廣泛應用在軟體設計中。

UML 結構

實踐GoF的23種設計模式:命令模式

場景上下文

在 簡單的分散式應用系統(示例程式碼工程)中,db 模組用來儲存服務註冊資訊和系統監控資料。其中,服務註冊資訊拆成了 profiles 和 regions 兩個表,在服務發現的業務邏輯中,通常需要同時操作兩個表,為了避免兩個表資料不一致的問題,db 模組需要提供事務功能:

實踐GoF的23種設計模式:命令模式

事務的核心功能之一是,當其中某個語句執行失敗時,之前已執行成功的語句能夠回滾,而使用命令模式能夠很方便地實現該功能。

程式碼實現

// demo/db/transaction.go
package db
// Command 執行資料庫操作的命令介面
// 關鍵點1: 定義命令抽象介面
type Command interface {
 // 關鍵點2: 命令抽象介面中宣告執行命令的方法
 Exec() error // Exec 執行insert、update、delete命令
 // 關鍵點3: 如果有撤銷功能,則需要定義Undo方法
 Undo() // Undo 回滾命令
 setDb(db Db) // SetDb 設定關聯的資料庫
}
// Transaction Db事務實現,事務介面的呼叫順序為begin -> exec -> exec > ... -> commit
// 關鍵點4: 定義Invoker物件
type Transaction struct {
    name string
 db   Db
 // 關鍵點5: Invoker物件持有Command的引用
 cmds []Command
}
// Begin 開啟一個事務
func (t *Transaction) Begin() {
 t.cmds = make([]Command, 0)
}
// Exec 在事務中執行命令,先快取到cmds佇列中,等commit時再執行
func (t *Transaction) Exec(cmd Command) error {
 if t.cmds == nil {
 return ErrTransactionNotBegin
 }
 cmd.setDb(t.db)
 t.cmds = append(t.cmds, cmd)
 return nil
}
// Commit 提交事務,執行佇列中的命令,如果有命令失敗,則回滾後返回錯誤
// 關鍵點6: 為Invoker物件定義Call方法,在方法內呼叫Command的執行方法Exec
func (t *Transaction) Commit() error {
 history := &cmdHistory{history: make([]Command, 0, len(t.cmds))}
 for _, cmd := range t.cmds {
 if err := cmd.Exec(); err != nil {
 history.rollback()
 return err
 }
 history.add(cmd)
 }
 return nil
}
// cmdHistory 命令執行歷史
type cmdHistory struct {
    history []Command
}
func (c *cmdHistory) add(cmd Command) {
 c.history = append(c.history, cmd)
}
// 關鍵點7: 在回滾方法中,呼叫已執行命令的Undo方法
func (c *cmdHistory) rollback() {
 for i := len(c.history) - 1; i >= 0; i-- {
 c.history[i].Undo()
 }
}
// InsertCmd 插入命令
// 關鍵點8: 定義具體的命令類,實現Command介面
type InsertCmd struct {
 // 關鍵點9: 命令通常持有接收者的引用,以便在執行方法中與接收者互動
 db         Db
 tableName string
 primaryKey interface{}
 newRecord interface{}
}
// 關鍵點10: 命令物件執行方法中,呼叫Receiver的Action方法,這裡的Receiver為db物件,Action方法為Insert方法
func (i *InsertCmd) Exec() error {
 return i.db.Insert(i.tableName, i.primaryKey, i.newRecord)
}
func (i *InsertCmd) Undo() {
 i.db.Delete(i.tableName, i.primaryKey)
}
func (i *InsertCmd) setDb(db Db) {
 i.db = db
}
// UpdateCmd 更新命令
type UpdateCmd struct {...}
// DeleteCmd 刪除命令
type DeleteCmd struct {...}

客戶端可以這麼使用:

func client() {
 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)
 if err := transaction.Commit(); err != nil {
 return ... 
 }
 return ...
}

總結實現命令模式的幾個關鍵點:

  1. 定義命令抽象介面,本例子中為 Command 介面。
  2. 在命令抽象介面中宣告執行命令的方法,本例子中為 Exec 方法。
  3. 如果要實現撤銷功能,還需要為命令物件定義 Undo 方法,在操作回滾時呼叫。
  4. 定義 Invoker 物件,本例子中為 Transaction 物件。
  5. 在 Invoker 物件持有 Command 的引用,本例子為 Command 的切片 cmds。
  6. 為 Invoker 物件定義 Call 方法,用於執行具體的命令,在方法內呼叫 Command 的執行方法 ,本例子中為 Transaction.Commit 方法。
  7. 如果要實現撤銷功能,還要在回滾方法中,呼叫已執行命令的 Undo 方法,本例子中為 cmdHistory.rollback 方法。
  8. 定義具體的命令類,實現 Command 介面,本例子中為 InsertCmd、UpdateCmd、DeleteCmd。
  9. 命令通常持有接收者的引用,以便在執行方法中與接收者互動。本例子中,Receiver 為 Db 物件。
  10. 最後,在命令物件執行方法中,呼叫 Receiver 的 Action 方法,本例子中, Receiver 的 Action 方法為 db.Insert 方法。

值得注意的是,本例子中 Transaction 物件在 Transaction.Exec 方法中只是將 Command 儲存在佇列中,只有當呼叫 Transaction.Commit 方法時才延遲執行相應的命令。

擴充套件

os/exec 中的命令模式

Go 標準庫的 os/exec 包也用到了命令模式。

package main
import (
 "os/exec"
)
// 對應命令模式中的Invoker
func main() {
 cmd := exec.Command("sleep", "1")
 err := cmd.Run()
}

在上述例子中,我們透過 exec.Command 方法將一個 shell 命令轉換成一個命令物件 exec.Cmd,其中的 Cmd.Run() 方法即是命令執行方法;而 main() 函式,對應到命令模式中的 Invoker;Receiver 則是作業系統執行 shell 命令的具體程式,從 exec.Cmd 的原始碼中可以看到:

// src/os/exec/exec.go
package exec
// 對應命令模式中的Command
type Cmd struct {
 ...
 // 對應命令模式中的Receiver
    Process *os.Process
 ...
}
// 對應命令模式中Command的執行方法
func (c *Cmd) Run() error {
 if err := c.Start(); err != nil {
 return err
 }
 return c.Wait()
}
func (c *Cmd) Start() error {
 ...
 // Command與Receiver的互動
 c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{...})
 ...
}
實踐GoF的23種設計模式:命令模式

CQRS 架構

CQRS 架構,全稱為 Command Query Responsibility Segregation,命令查詢職責隔離架構。CQRS 架構是微服務架構模式中的一種,它利用事件(命令)來維護從多個服務複製資料的只讀檢視,透過讀寫分離思想,提升微服務架構下查詢的效能。

實踐GoF的23種設計模式:命令模式

CQRS 架構可分為 命令端 和 查詢端,其中命令端負責資料的更新;查詢端負責資料的查詢。命令端的寫資料庫在資料更新時,會向查詢端的只讀資料庫傳送一個同步資料的事件,保證資料的最終一致性。

其中的命令端,就使用到了命令模式的思想,將資料更新請求封裝成命令,非同步更新到寫資料庫中。

典型應用場景

  • 事務模式。事務模式下往往需要 Undo 操作,使用命令模式實現起來很方便。
  • 遠端執行。Go 標準庫下的 exec.Cmd、http.Client 都屬於該型別,將請求封裝成命令來執行。
  • CQRS 架構。微服務架構模式中的一種,透過命令模式來實現資料的非同步更新。
  • 延遲執行。當你希望一個操作能夠延遲執行時,通常會將它封裝成命令,然後放到一個佇列中。

優缺點

優點

  1. 符合單一職責原則。在命令模式下,每個命令都是職責單一、松耦合的;當然也可以透過組合的方式,將多個簡單的命令組合成一個負責的命令。
  2. 可以很方便地實現操作的延遲執行、回滾、重做等。
  3. 在分散式架構下,命令模式能夠方便地實現非同步的資料更新、方法呼叫等,提升效能。

缺點

  1. 命令模式下,呼叫往往是非同步的,而非同步會導致系統變得複雜,問題出現時不好定位解決。
  2. 隨著業務越來越複雜,命令物件也會增多,程式碼會變得更難維護。

與其他模式的關聯

在實現 Undo/Redo 操作時,你通常需要同時使用 命令模式 和 備忘錄模式。

另外,命令模式 也常常和觀察者模式 一起出現,比如在 CQRS 架構中,當命令端更新資料庫後,寫資料庫就會透過事件將資料同步到讀資料庫上,這裡就用到了觀察者模式。

文章配圖

可以在 用Keynote畫出手繪風格的配圖 中找到文章的繪圖方法。

參考

[1] 【Go實現】實踐GoF的23種設計模式:SOLID原則, 元閏子

[2] 【Go實現】實踐GoF的23種設計模式:觀察者模式, 元閏子

[3] Design Patterns, Chapter 5. Behavioral Patterns, GoF

[4] 命令模式refactoringguru.cn

[5] The command pattern in Go, rolandjitsu

[6] CQRS 模式, microsoft azure

[7] CQRS Design Pattern in Microservices Architectures, Mehmet Ozkaya

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章