實踐Go的命令模式

簡介

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

因此,我們需要對請求進行抽象,將上下文信息封裝到請求對象里,這其實就是命令模式,而該請求對象就是 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,請求的接收方。

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

UML 結構

場景上下文

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

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

代碼實現

// 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 stringdb   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         DbtableName stringprimaryKey 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 {...// 對應命令模式中的ReceiverProcess *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{...})...
}

CQRS 架構

CQRS 架構,全稱為 Command Query Responsibility Segregation,命令查詢職責隔離架構。CQRS 架構是微服務架構模式中的一種,它利用事件(命令)來維護從多個服務復制數據的只讀視圖,通過讀寫分離思想,提升微服務架構下查詢的性能。

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

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

典型應用場景

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

優缺點

優點

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

缺點

  1. 命令模式下,調用往往是異步的,而異步會導致系統變得復雜,問題出現時不好定位解決。
  2. 隨著業務越來越復雜,命令對象也會增多,代碼會變得更難維護。

與其他模式的關聯

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

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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/37620.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/37620.shtml
英文地址,請注明出處:http://en.pswp.cn/web/37620.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Bootstrap更改默認的“請在電子郵件地址中包含@”

摘要: 今天開發一個外貿系統,必須全部英文的,但是使用到bootatrp 4的input標簽的type"email"輸入沒有含“”符號時會提示:“請在電子郵件地址中包含”中文提示!一開始以為是中國下載的谷歌是瀏覽器自帶的提示…

[思考記錄.產品改進]假如異常日志可以自動上報

最近考慮日志收集的事情,主要出發點是: 1、在問題出現后能方便快速地收集相關的線索和證據,幫助快速定位和解決問題。因為反饋問題往往在發生之后,如果在這個時候能快速方便地拿到有用信息是件很舒服的事情,而在獲取日…

AIGC重塑創意設計:不僅能帶來新技術,更能引發新思考

隨著科技的飛速發展,AIGC(生成式人工智能)已經逐漸成為創意設計領域的一股新勢力。從影視制作到游戲設計,從平面廣告到數字媒體,AIGC的影響力無處不在,它不僅帶來了全新的技術手段,更在深層次上…

Linux-筆記 嵌入式gdb遠程調試

目錄 前言 實現 1、內核配置 2、GDB移植 3、準備調試程序 4、開始調試 前言 gdb調試器是基于命令行的GNU項目調試器,通過gdb工具我們可以實現許多調試手段,同時gdb支持多種語言,兼容性很強。 在桌面 Linux 系統(如 Ubuntu、Cent…

跨越地域界限:Eureka實現跨區域服務發現全解析

跨越地域界限:Eureka實現跨區域服務發現全解析 在微服務架構的浪潮中,服務的分布式部署已成為常態。隨著業務的全球擴展,服務往往需要跨區域甚至跨國界進行部署。在這樣的背景下,服務發現機制面臨著新的挑戰——如何實現不同區域…

如何在服務器中找到數據庫文件路徑

在服務器中找到數據庫文件路徑的具體方法取決于您所使用的數據庫管理系統和服務器的操作系統。以下是一些常見的數據庫系統(如MySQL、Microsoft SQL Server、Oracle、PostgreSQL和MongoDB)的文件路徑查找方法的詳細步驟: MySQL 通過命令行查…

Redis基礎教程(三):redis命令

💝💝💝首先,歡迎各位來到我的博客,很高興能夠在這里和您見面!希望您在這里不僅可以有所收獲,同時也能感受到一份輕松歡樂的氛圍,祝你生活愉快! 💝&#x1f49…

08 - Python面向對象編程進階

面向對象進階 在前面的章節我們已經了解了面向對象的入門知識,知道了如何定義類,如何創建對象以及如何給對象發消息。為了能夠更好的使用面向對象編程思想進行程序開發,我們還需要對Python中的面向對象編程進行更為深入的了解。 property裝…

六西格瑪綠帶培訓的證書有什么用處?

近年來,六西格瑪作為一套嚴謹而系統的質量管理方法,被廣泛運用于各行各業。而六西格瑪綠帶培訓證書,作為這一方法論中基礎且重要的認證,對于個人和企業而言,都具有不可忽視的價值。本文將從多個角度深入探討六西格瑪綠…

重寫功能 rewrite

Nginx服務器利用 ngx_http_rewrite_module 模塊解析和處理rewrite請求,此功能依靠 PCRE(perl compatible regular expression),因此編譯之前要安裝PCRE庫,rewrite是nginx服務器的重要功能之 一,用于實現URL的重寫,URL的…

充電寶口碑哪個好?好用充電寶品牌有哪些?好用充電寶推薦

充電寶作為我們日常生活和出行的重要伙伴,其品質和性能直接影響著我們的使用體驗。今天,就來和大家探討一下充電寶口碑哪個好,為大家盤點那些備受贊譽的好用充電寶品牌,并向您推薦幾款值得入手的充電寶,外出時不再擔心…

mac英語學習工具:Eudic歐路詞典 for Mac 激活版

Eudic歐路詞典是一款非常受歡迎的英語學習軟件,它提供了豐富的詞匯解釋、例句、同義詞、反義詞等功能,幫助用戶更好地理解和掌握英語單詞。 以下是Eudic歐路詞典的一些主要特點: 海量詞匯庫:Eudic歐路詞典擁有龐大的詞匯庫&#…

flutter photo_manager 報錯:Error: ‘DecoderCallback‘ isn‘t a type.

看看是不是你的photo_manager版本少于3.0。如果是少于3.0,請及時升級到3.0及以上版本,同時因為photo_manager3.0不再提供 AssetEntityImageProvider 這個方法,會導致報錯,不要慌,請添加下方的庫即可解決問題&#xf…

為什么人人都要懂一些銷售思維

創業看事情的高度、考慮的維度比做銷售更高、更復雜、更全面,銷售思維、銷售方法更多時候用在解決局部問題,幫我們打局部戰爭,它是術。 但是,我仍然認為,銷售思維是一種很有用、有效、有力量、必要的思維。我們每個人…

面試題002-Java-Java集合

面試題002-Java-Java集合 目錄 面試題002-Java-Java集合題目自測題目答案1. 說說 List,Set,Map 三者的區別?三者底層的數據結構?2. 有哪些集合是線程不安全的?怎么解決呢?3. 比較 HashSet 、LinkedHashSet 和 TreeSet 三者的異同&…

簡過網:考一建需要報培訓班嗎?報班費用是多少錢

近幾年來,越來越多的朋友都開始關注和參與備考一建,那么,大家在備考一建時,都報培訓班了嗎?報班的費用是多少錢?接下來,我們一塊來了解一下吧? ? 一、考一建需要報培訓班嗎&#…

【Jetpack】Lifecycle之自定義LifecycleOwner

Lifecycle設計講解 Lifecycle的設計其實十分簡單,主要就是Lifecycle對象和LifecycleOwner接口 Lifecycle用于記錄對象的生命周期,以及在生命周期發生改變時通知外部 LifecycleOwner用于表示對象具備生命周期管理能力 LifecycleOwner的實現方式很簡單&…

賽目科技三度遞表:凈利率及資產回報率不斷下滑,經營成本越來越高

《港灣商業觀察》施子夫 5月29日,北京賽目科技股份有限公司(以下簡稱,賽目科技)第三次遞表港交所,公司擬主板上市,獨家保薦機構為光銀國際。 公開信息顯示,賽目科技此前曾于2022年12月&#x…

Java全套智慧校園系統源碼:核心功能、發展趨勢、基于電子班牌: Android 7.1+小程序:原生開發+多學校Saas 模式

Java全套智慧校園系統源碼:核心功能、發展趨勢、基于電子班牌: Android 7.1小程序:原生開發多學校Saas 模式 智慧校園系統是一個集成了多種功能的綜合性平臺,旨在提升校園內的教學、管理、服務等方面的效率和體驗。那么&#xff…

比較(五)利用python繪制棒棒糖圖

比較(五)利用python繪制棒棒糖圖 棒棒糖圖(Lollipop plot)簡介 棒棒糖圖實際上是修飾后的條形圖。當在處理大量的值,并且當這些值都很高時,棒棒糖圖就很有用。 快速繪制 基于matplotlib import pandas as…