控制器模式 | Controller

2024-08-28
GRASP

開開心心寫完 mentor 出的作業後,被告知要前後端分離

開開心心寫完 mentor 出的作業後,被告知要前後端分離,圖片來源

情境

前後端分離後,會遇到一個問題,也就是做為後端,誰負責作為第一個物件去接收來自前端的請求,並進行系統操作。

比如說一個網路書店的 API ,允許客戶添加新書籍到我的最愛,當使用者點下書籍圖案旁邊的愛心的時候,來自前端請求傳到後端,哪個物件負責處理這個請求呢 ?


控制器是什麼 ?

“ 控制器 (Controller) 是 UI 層之上的第一個物件,它負責接收和處理系統操作消息。 ”

― Craig Larman, Applying UML and Patterns 3rd, ch17.13

作為第一個物件去接收來自前端的請求,並進行系統操作,控制器提供了兩個思路

  1. 代表整個系統的物件或根物件 (Root Entity)
  2. 虛構一個代表某個用例的物件,該物件代表了這個案例,此時該物件稱為用例控制器。

根物件通常是已有的領域概念,因此並不會增加系統的耦合,然而根物件有可能在後續維護與新增特性的過程中,變得逐漸臃腫。

利用純虛構的用例控制器可以有效緩解,但是要注意系統是否足以複雜到需要用例控制器,因為用例控制器屬於純虛構的物件。

根物件控制器

根物件作為控制器,可以在不增加系統耦合的情況下,作為第一個物件去接收來自前端的請求,並進行系統操作,提供一種簡潔的解決方案。

大老二領域模型

大老二領域模型

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

出牌

出牌交互圖

例如在出牌這個用例中,Game 接收到玩家出的牌後

  1. 調用 VaildHand 去識別手牌類型是單張、對子還是順子後並驗證是否為本場遊戲規範合理的卡牌組合。
  2. 調用 CurrentValidHand 去比較點數是否較大。
  3. 調用 CurrentPlayer 去丟棄該手牌。
  4. 調用 CurrentValidHand 更新當前遊戲中的卡牌組合。
  5. 調用 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 處理的方案。

但對於想要打造一個易懂的程式,如果其用例較少,利用根物件作為控制器的方案無疑是最適合的。


參考資料

也可以看看以下文章