Go 設計模式 -- 命令模式

大家好,這裏是每週都陪你進步的網管,假期歸來咱們繼續更新設計模式系列,這次要和大家一起學習的是命令模式,如果你對領域驅動設計感興趣,這個模式一定要好好學,命令模式是 DDD 風格的框架中高頻使用的一個模式。

命令模式是一種行爲型模式。它通過將請求封裝爲一個獨立的對象即命令對象,來解耦命令的調用者和接收者,使得調用者和接收者不直接交互。在命令對象裏會包含請求相關的全部信息,每一個命令都是一個操作的請求: 請求方發出請求要求執行一個操作; 接收方收到請求,並執行操作。

命令模式的構成

命令模式中有如下必須存在的基礎組件:

直接這麼描述聽起來比較抽象,下面我們結合 UML 類圖詳細看一下命令模式內部這幾種基礎組件的特性和具有的行爲。

UML 類圖

命令模式的構成如下圖所示

請求的接收者 Receiver 我們做了簡化,根據實際場景複雜度的需要我們也可以進一步抽象出接口和實現類,圖中表示的命令模式一共由五種角色構成,下面詳細解釋下它們各自的特性和具有的行爲

  1. 發送者(Invoker)負責對請求進行初始化, 其中必須包含一個成員變量來存儲對於命令對象的引用。 發送者觸發命令, 而不是向接收者直接發送請求。 發送者並不負責創建命令對象,而是由客戶端負責調用構造函數創建命令對象。

  2. 命令接口(Command) 通常接口中僅聲明一個執行命令的方法 Execute()。

  3. 具體命令 (Concrete Commands) 會實現各種類型的請求。 命令對象自身並不完成工作, 而是會將調用委派給一個接收者對象。 接收者對象執行方法所需的參數可以聲明爲具體命令的成員變量。 一般會約定命令對象爲不可變對象, 僅允許通過構造函數對這些成員變量進行初始化。

  4. 接收者 (Receiver) 處理業務邏輯的類。 幾乎任何對象都可以作爲接收者。 命令對象只負責處理如何將請求傳遞到接收者的細節, 接收者自己會完成實際的工作。

  5. 客戶端 (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 層進行處理從而完成業務邏輯。

最後我們再來總結一下命令模式的優缺點。

命令模式的優點

  1. 通過引入中間件(抽象接口),解耦了命令請求與實現。

  2. 擴展性良好,可以很容易地增加新命令。

  3. 支持組合命令,支持命令隊列。

  4. 可以在現有命令的基礎上,增加額外功能。比如日誌記錄,結合裝飾器模式會更加靈活。

命令模式的缺點

  1. 具體命令類可能過多。

  2. 命令模式的結果其實就是接收方的執行結果,但是爲了以命令的形式進行架構、解耦請求與實現,引入了額外類型結構(引入了請求方與抽象命令接口),增加了理解上的困難。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/n1R1fnRZuDwlaQqsDh5y3g