命令模式:若只如“初見”

版權聲明:本文由本人撰寫並發表於InfoQ, 原文鏈接: http://www.infoq.com/cn/news/2018/01/Command-mode-if-only-first-see

似曾相識

最近在InfoQ上看到一篇談論命令模式與CQRS架構的譯文《From CQS to CQRS》(建議先閱讀此文,本文會針對該文的一些觀點進行探討),文章從命令模式談起,然後提出了命令模式的升級版——命令總線:

這裏寫圖片描述

這個圖總給人一種似曾相識的感覺,仔細回想了一下,發覺這不正是Struts2架構中的核心部分嗎?

這裏寫圖片描述

作爲一個基於命令模式的MVC框架,Struts2在對於命令的處理上和命令總線的設計如出一轍,它的ActionInvocation和Interceptor對應的正是命令總線裏CommandBus和Decorator。應該說兩者是因爲遵循了同樣的OO設計準則纔會如此高度的一致,可見優雅的設計都是相似的,模式之所以成爲模式是有必然性的。

命令模式“錯”了嗎?

關於命令模式的詳細介紹可以參考四人幫的《設計模式》和《Head First Design Patterns》,本文不做過多贅述。命令模式的核心設計用意是對調用端和執行端進行解耦,這種解耦是非常徹底的,即:在調用端不會出現任何執行端的API,甚至在Command這個核心抽象上的execute方法上都是不帶任何參數的:

public interface Command {
    public void execute();
}

這使得Command對於調用方而言完全是一個“黑盒”,調用方只知道下達命令,對於它將被如何執行毫不知情,命令的解釋與執行是由命令和執行方共同完成的,這符合現實世界中很多事物之間的協作關係,確保了參與其中的各方職責單一,分工明確,否則角色混亂,各種事情攪在一起就會變成一團糟。

《From CQS to CQRS》一文爲引出命令總線在介紹命令模式時闡述了它的一個“缺陷”:即由於命令模式的“強”封裝使得它不能很好地“包裹”數據,也就是命令參數。文章稱這些經常變化的命令參數既不能通過execute方法傳遞,又不適合作爲命令類構造函數的參數,因此作者認爲命令模式是“有問題”的,需要進行重構,而重構的結果就是命令總線。

然而文章對命令模式的diss是站不住腳的,因爲即使按照作者推薦的方式將所謂變化的部分(即“數據”)抽離到一個DTO中,在調用端依然需要實例化它,爲其設定各種參數,這些參數天然就是執行業務的前提,無論採用何種設計,作爲下達命令的一方“把要乾的事情講明白”是最起碼的“份內事”,所以在命令類的構造函數上傳遞參數和剝離到一個單獨的DTO中包裹數據沒有任何本質的區別,後者的做法反而有“從富領域模型向貧血的領域模型開倒車”的嫌疑。

所以命令模式並沒有錯,命令總線也不是爲解決命令模式所謂的“弊端”而來,它實際上是應更大的架構目標和應用場景而產生的。

更大的格局

原生的命令模式在它所適用的場景上表現自然是完美的,這些場景大多數是領域模型的一些“局部”,命令的類型和邏輯都是和業務緊密聯繫的。而另一方面,人們也認識到命令模式具有廣泛的適用性,具備在更高級別的架構模式中扮演核心角色的能力,但是將命令模式提升到更加通用和完備的層面還需要解決以下一些問題:

  1. 將命令的“數據”和“邏輯”剝離開,形成通用的“命令”和“命令處理機制”

    在原生的命令模式裏,每一個具體的命令類都會包含特定的字段和邏輯,通用化處理的第一步就需要把命令的數據和行爲剝離開,數據剝離之後可以使用通用的數據結構如Map或更加抽象的類型如Object來替換,而行爲上的通用化處理則要依靠下面幾點來實現。

  2. 抽象統一的命令處理流程

    在一個特定的框架或業務系統裏,命令的執行往往都有一定的“套路”,如果想讓命令的執行通用化,勢必要精心地總結和歸納各種命令在執行上的共性,提煉出一個通用的程序執行的“流程”,這個所謂的“流程”就是服務總線模式中的CommandBus和Struts2中的ActionInvocation,統一處理流程可以包含大量豐富的主題,比如日誌、事務處理、安全攔截、性能跟蹤、數據校驗等等。

  3. 基於配置的流程定義與組裝

    但是統一的處理流程並不意味着只能有一種,也不意味着一成不變,爲了讓流程處理具有廣泛的適用性,通過配置的方式去定義和組裝命令的處理流程是非常必要的,這樣可以讓流程變得靈活,可定製,流程中的環節也都是可插拔的,就如同Struts2使用struts.xml去描述interceptors棧和action那樣。

  4. 提供命令處理的公共基礎設施

    當統一的“流程”抽象出來之後,需要針對普遍存在的“環節”提供公共實現,例如前文命令總線上示意的LoggingDecorator、ValidationDecorator等一系列的裝飾器和Struts2中的logger、validation等一系列的Interceptor,這些都會作爲命令處理過程中的“公共基礎設施”,一環一環地套接起來,讓每一個命令逐一經過這些“環節”進行相應的處理。這種工作模式和麪向切面編程中的“Around Advice”機制是完全一致的。

  5. 給自定義命令處理邏輯留下接口

    無論如何,這處理流程上的最後一環必定是留給命令“執行者”的,連同封裝好的數據一起,落腳到一個回調的接口上,讓命令“執行者”們補上屬於它們的應盡之責:業務處理代碼,則整個命令處理流程的“閉環”就算大功告成了。
    原生的命令模式往往應用在領域模型上,與業務緊密關聯,而命令總線的意圖則是試圖將命令模式提升到架構層面,在整個系統的某些“分層”(layer)之間建立一種一致的全局的通信模式,從而實現“層間解耦”,例如像Struts2那樣在MVC的視圖層與模型層之間組織和傳遞Action。爲了實現這一目標,勢必要對原生的命令模式進行改進,甚至是妥協,比如將命令的數據與行爲進行拆分,這確實像是“從富領域模型向貧血的領域模型開倒車” ,但是爲了實現更大的架構目標,局部的妥協是必須的,也是值得的。

終極產物

如從一條小溪最終匯入江河大海,命令模式被提升爲命令總線之後進而又參與到了CQRS架構中,成爲組成這一先進架構的核心模式之一,這也可以視爲命令模式進化到現在的“終極產物”。CQRS架構的核心思想是把系統和外界的信息交換進行了讀寫分離,在數據寫入時,通過構建富領域模型進行業務計算,這是領域驅動設計擅長的領域,在這個過程中“命令”是驅動領域模型運轉的鑰匙。在數據讀取時,CQRS會繞過領域模型直接從持久層提取數據,這有助於提升性能,同時減輕領域模型的壓力。
但是CQRS已經不再是本文關注的重點了,因爲CQRS直接複用了命令總線,沒有做其他的提升,本文寫作的主要目的是想回顧命令模式從起源到終極產物的演化歷程,闡述這些演化背後的真正用意以及實現這些目標的寶貴設計思想。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章