控制器模式 | Controller

開開心心寫完 mentor 出的作業後,被告知要前後端分離,圖片來源
情境
前後端分離後,會遇到一個問題,也就是做為後端,誰負責作為第一個物件去接收來自前端的請求,並進行系統操作。
比如說一個網路書店的 API ,允許客戶添加新書籍到我的最愛,當使用者點下書籍圖案旁邊的愛心的時候,來自前端請求傳到後端,哪個物件負責處理這個請求呢 ?
控制器是什麼 ?
“ 控制器 (Controller) 是 UI 層之上的第一個物件,它負責接收和處理系統操作消息。 ”
作為第一個物件去接收來自前端的請求,並進行系統操作,控制器提供了兩個思路
- 代表整個系統的物件或根物件 (Root Entity)
- 虛構一個代表某個用例的物件,該物件代表了這個案例,此時該物件稱為用例控制器。
根物件通常是已有的領域概念,因此並不會增加系統的耦合,然而根物件有可能在後續維護與新增特性的過程中,變得逐漸臃腫。
利用純虛構的用例控制器可以有效緩解,但是要注意系統是否足以複雜到需要用例控制器,因為用例控制器屬於純虛構的物件。
根物件控制器
根物件作為控制器,可以在不增加系統耦合的情況下,作為第一個物件去接收來自前端的請求,並進行系統操作,提供一種簡潔的解決方案。

大老二領域模型
在上圖,我們能看到 Game 這個概念作為整個領域模型的根物件,因此無論是玩家加入或者出牌,Game 作為第一個物件去接收請求,並分配並調用給那些聚合成為他的物件。

出牌交互圖
例如在出牌這個用例中,Game 接收到玩家出的牌後
- 調用 VaildHand 去識別手牌類型是單張、對子還是順子後並驗證是否為本場遊戲規範合理的卡牌組合。
- 調用 CurrentValidHand 去比較點數是否較大。
- 調用 CurrentPlayer 去丟棄該手牌。
- 調用 CurrentValidHand 更新當前遊戲中的卡牌組合。
- 調用 CurrentPlayer 設定下一名玩家。
Game 這個根物件在以上範例中,達成了接收請求,並調用系統中的物件去完成請求。
用例控制器
隨著業務發展,聚合不是一次就會出現,而是會逐漸發酵,這導致根物件的職責會逐漸膨脹,需要控制的用例越來越多。
又或者在分析時,發現可以將系統分成許多聚合,彼此關聯性較低。
此時可以考慮替每個用例都虛構出一個控制器,作為用例控制器,並讓用例控制器以用例為最小顆粒度,作為第一個物件去接收來自前端的請求,並進行系統操作。
控制器不是什麼 ?
Controller這個名稱,以及他做為系統的代理物件,很容易跟以下概念搞混
- MVC 中的 Controller
- DDD 中的 Application Layer
- 設計模式中的 Facade 模式與 Command 模式
不是 MVC 的 Controller
MVC 的 Controller 與 Model、View 進行交互。
而 GRASP 的 Controller 關注的是接收到請求後,整個系統的操作與交互,並沒有一個 View 需要控制。
不是 Application Layer
領域驅動設計中的 Application Layer 負責接收來自用戶的請求並且調用領域物件進行操作。
當比較對象是做為根物件的 Controller ,因為是根物件所以必然是有狀態的,這與 DDD 中的 Application Layer 的無狀態不一致。
而當比較對象是作為用例控制器,用例控制器是否有狀態並不是一種設計目的,而也因為如此,如果用例控制器無狀態,可以視為一種 Application。
不是 Facade、Command
設計模式中 Facade Pattern、Command Pattern 前者是設計接口的簡化系統,後者是針對封裝請求以便進行請求的多樣化操作。
這與 GRASP 的 Controller 設計目的不同,也就是系統的操作與交互,Facade Pattern 較關注簡化系統的接口以便前端使用;Command 較關注對於請求的不同處理方式。
作者心得
Controller模式替前後端分離中的後端提供了誰是第一個物件來處理請求,並處理系統操作消息的原則。
雖然在現實狀況中,有更複雜的情況,因此出現 Command Pattern 這種專注處理請求的方案,也有 Application Layer 這種將請求消化後轉手給 Aggreagte Root 處理的方案。
但對於想要打造一個易懂的程式,如果其用例較少,利用根物件作為控制器的方案無疑是最適合的。