Go設計模式--命令模式
大家好,這里是每周都陪你進步的網管,假期歸來咱們繼續更新設計模式系列,這次要和大家一起學習的是命令模式,如果你對領域驅動設計感興趣,這個模式一定要好好學,命令模式是DDD風格的框架中高頻使用的一個模式。
命令模式是一種行為型模式。它通過將請求封裝為一個獨立的對象即命令對象,來解耦命令的調用者和接收者,使得調用者和接收者不直接交互。在命令對象里會包含請求相關的全部信息,每一個命令都是一個操作的請求: 請求方發出請求要求執行一個操作; 接收方收到請求,并執行操作。
命令模式的構成
命令模式中有如下必須存在的基礎組件:
- Receiver:命令的接收方,唯一包含業務邏輯的類,命令對象會將請求傳遞給它,它是請求的最終處理者
- Command:命令對象,組裝了一個Receiver成員,并綁定實現了Receiver的一個特定行為的調用
- Invoker:請求的發送者,組裝了Command成員,通過調用Command實例的execute()方法來觸發對應的指令
- Client:通過將Receiver實例和請求信息傳遞給Command構造器來創建Command對象,之后會將創建的對象同Invoker綁定。
直接這么描述聽起來比較抽象,下面我們結合UML類圖詳細看一下命令模式內部這幾種基礎組件的特性和具有的行為。
UML類圖
命令模式的構成如下圖所示

請求的接收者Receiver我們做了簡化,根據實際場景復雜度的需要我們也可以進一步抽象出接口和實現類,圖中表示的命令模式一共由五種角色構成,下面詳細解釋下它們各自的特性和具有的行為
- 發送者(Invoker)負責對請求進行初始化, 其中必須包含一個成員變量來存儲對于命令對象的引用。 發送者觸發命令, 而不是向接收者直接發送請求。 發送者并不負責創建命令對象,而是由客戶端負責調用構造函數創建命令對象。
- 命令接口(Command) 通常接口中僅聲明一個執行命令的方法 Execute()。
- 具體命令 (Concrete Commands) 會實現各種類型的請求。 命令對象自身并不完成工作, 而是會將調用委派給一個接收者對象。 接收者對象執行方法所需的參數可以聲明為具體命令的成員變量。 一般會約定命令對象為不可變對象, 僅允許通過構造函數對這些成員變量進行初始化。
- 接收者 (Receiver) 處理業務邏輯的類。 幾乎任何對象都可以作為接收者。 命令對象只負責處理如何將請求傳遞到接收者的細節, 接收者自己會完成實際的工作。
- 客戶端 (Client) 會創建并配置具體命令對象。 客戶端必須將包括接收者對象在內的所有請求參數傳遞給命令對象的構造函數, 完成命令與執行操作的接收者的關聯。
發送者是通常我們能接觸到的終端,比如電視的遙控器,點擊音量按鈕發送加音量的命令,電視機里的芯片就是接收者負責完成音量添加的處理邏輯。
下面我們通過一個讓PS5完成各種操作的例子,結合Golang代碼實現理解一下用代碼怎么實現命令模式。
代碼示例
假設PS5的CPU支持A、B、C三個命令操作,
"本文使用的完整可運行源碼
去公眾號「網管叨bi叨」發送【設計模式】即可領取"
type CPU struct{}
func (CPU) ADoSomething() {
fmt.Println("a do something")
}
func (CPU) BDoSomething() {
fmt.Println("b do something")
}
type PS5 struct {
cpu CPU
}
func (p PS5) ACommand() {
p.cpu.ADoSomething()
}
func (p PS5) BCommand() {
p.cpu.ADoSomething()
}
func main() {
cpu := CPU{}
ps5 := PS5{cpu}
ps5.ACommand()
ps5.BCommand()
}后續還可能會給CPU增加其他命令操作,以及需要支持命令宏(即命令組合操作)。如果每次都修改PS5的類定義,顯然不符合面向對象開閉原則(Open close principle)的設計理念。
通過命令模式,我們把PS5抽象成命令發送者、CPU對象作為執行業務邏輯的命令接收者,然后引入引入Command 接口把兩者做解耦,來滿足開閉原則。
下面看一下用命令模式解耦后的代碼實現,模式中各個角色的職責、實現思路等都在代碼注釋里做了標注,咱們直接看代碼吧。
"本文使用的完整可運行源碼
去公眾號「網管叨bi叨」發送【設計模式】即可領取"
// 命令接收者,負責邏輯的執行
type CPU struct{}
func (CPU) ADoSomething(param int) {
fmt.Printf("a do something with param %v\n", param)
}
func (CPU) BDoSomething(param1 string, param2 int) {
fmt.Printf("b do something with params %v and %v \n", param1, param2)
}
func (CPU) CDoSomething() {
fmt.Println("c do something with no params")
}
// 接口中僅聲明一個執行命令的方法 Execute()
type Command interface {
Execute()
}
// 命令對象持有一個指向接收者的引用,以及請求中的所有參數,
type ACommand struct {
cpu *CPU
param int
}
// 命令不會進行邏輯處理,調用Execute方法會將發送者的請求委派給接收者對象。
func (a ACommand) Execute() {
a.cpu.ADoSomething(a.param)
a.cpu.CDoSomething()// 可以執行多個接收者的操作完成命令宏
}
func NewACommand(cpu *CPU, param int) Command {
return ACommand{cpu, param}
}
"本文使用的完整可運行源碼
去公眾號「網管叨bi叨」發送【設計模式】即可領取"
type BCommand struct {
state bool // Command 里可以添加些狀態用作邏輯判斷
cpu *CPU
param1 string
param2 int
}
func (b BCommand) Execute() {
if b.state {
return
}
b.cpu.BDoSomething(b.param1, b.param2)
b.state = true
b.cpu.CDoSomething()
}
func NewBCommand(cpu *CPU, param1 string, param2 int) Command {
return BCommand{false,cpu, param1, param2}
}
type PS5 struct {
commands map[string]Command
}
// SetCommand方法來將 Command 指令設定給PS5。
func (p *PS5) SetCommand(name string, command Command) {
p.commands[name] = command
}
// DoCommand方法選擇要執行的命令
func (p *PS5) DoCommand(name string) {
p.commands[name].Execute()
}
func main() {
cpu := CPU{}
// main方法充當客戶端,創建并配置具體命令對象, 完成命令與執行操作的接收者的關聯。
ps5 := PS5{make(map[string]Command)}
ps5.SetCommand("a", NewACommand(&cpu, 1))
ps5.SetCommand("b", NewBCommand(&cpu, "hello", 2))
ps5.DoCommand("a")
ps5.DoCommand("b")
}本文的完整源碼,已經同步收錄到我整理的電子教程里啦,可向我的公眾號「網管叨bi叨」發送關鍵字【設計模式】領取。

公眾號「網管叨bi叨」發送關鍵字【設計模式】領取。
總結
關于命令模式的學習和實踐應用,推薦有Java背景的同學看一下阿里開源的框架COLA 3.0,里面融合了不少DDD的概念,其中的Application層主要就是各種Command、Query對象封裝了客戶端的請求,它們的Execute方法負責將請求轉發給Domain層進行處理從而完成業務邏輯。
最后我們再來總結一下命令模式的優缺點。
命令模式的優點
- 通過引入中間件(抽象接口),解耦了命令請求與實現。
- 擴展性良好,可以很容易地增加新命令。
- 支持組合命令,支持命令隊列。
- 可以在現有命令的基礎上,增加額外功能。比如日志記錄,結合裝飾器模式會更加靈活。
命令模式的缺點
- 具體命令類可能過多。
- 命令模式的結果其實就是接收方的執行結果,但是為了以命令的形式進行架構、解耦請求與實現,引入了額外類型結構(引入了請求方與抽象命令接口),增加了理解上的困難。





























