前言
我們可以將系統簡單看作是由三個元件所組成: UI、業務規則和資料庫,對於一些簡單的系統來說,這樣已經足夠了,但對於大部分的系統來說,元件的數量要比它來的多。
Hunt the Wumpus 冒險遊戲
這是一款讓玩家在console上輸入指令(如:GO EAST、SHOOT WEST)後,電腦就會回應玩家看到、聞到、聽到和體驗到什麼東西,假設我們要保留基於文字的UI,但要將其與遊戲規則解偶開來,以便我們的遊戲可以輕易的在各國中以不同的語言執行,遊戲規則與UI的通訊是透過使用和語言無關的API,而UI會將API輸出結果轉換成適當的語系作呈現。
如果原始碼的依賴關係被正確管理,如下圖,那任何數量的UI元件都可以使用相同的遊戲規則,遊戲規則也不知道也不關心目前是哪一個國家的人在使用。
接著,我們再次假設遊戲的狀態是保存在某種持久性儲存體上,或許是flash記憶體、雲端、也或者是RAM當中,不論哪種情況,我們都不希望遊戲規則知道這個資訊,因此,我們在建立一個API,使遊戲規則可以使用該API與資料儲存元件進行通訊,如下圖。
可否採用整潔的架構?
上述遊戲的案例已經具備採用整潔架構的一切了,包括使用案例、邊界、實體和相對應的資料結構,但我們是否真的找到所有重要的架構邊界呢?
假設,語系並非UI上面唯一的變化因素,我們可能想改變文字的通訊方式,如用一個一般一點的SHELL視窗,或者聊天室應用程式,總之,可能存在多種可能性,於是我們多了些設計如下:
虛線框表示它是定義了一個API的抽象元件,該API會由其附近的元件來實作(如English實作Language API),另外看箭頭的方向可知,GameRules與Language之間的交互(通訊)是由GameRules定義的規則與實作Language API的元件來完成的。同樣的,Language與TextDelievery的交互也是通過由Language定義與實作TextDelivery API的元件來完成的,所以結論是,API的定義和維護都是由使用芳來負責的,而非實現方,在每一種情況下,由這些Boundary介面定義的API,其擁有者都是上游元件。
舉例: 誰要用誰就要負責維護,譬如業務邏輯會用到DB裡面的資料做運算,所以該資料的介面應該是定義在業務邏輯層,並由DB層的元件去實作它。
讓我們只保留API來簡化一下架構圖,如下所示:
上圖的架構可分為左側與右側兩個資料流,左側關注在與使用者的通訊,右側則關注資料的持久性,兩個部分最終都會在頂部相遇,以GameRules作為最終處理器。
注意,箭頭是指原始碼的依賴方向,而非資料流的方向
交匯資料流
任何系統都只有兩個資料流嗎? 當然不是,想像一下如果我們還需要網路元件,就會變成是三個資料流,如下所示:
所以隨著系統越來越複雜,元件結構就會被分割成更多的部分。
資料流的分割
如果你覺得所有的資料流最終都會彙整到一個組件上,那就太簡單了呀,但現實的架構往往不如你所願,譬如在Hunt the Wumpus這款遊戲中,有一部份的遊戲規則是處理有關地圖的機制,包括洞穴是如何連接的,以及每個洞穴中有哪些物體或怪獸,也掌管玩家必須處理的事件,此外,還有更高層級的遊戲規則,就是掌管玩家血量狀態的遊戲規則與特定事件的成本或收益,這些策略會讓玩家逐漸失血,或是發現食物來補血。
所以,這麼多的遊戲規則間,也可以區分為低層級或高層級的遊戲規則,而高層級的遊戲規則應該是那些根據玩家的狀態來決定輸贏的部分,如下圖:
只有這樣嗎? 讓我們加入微服務讓事情變得更有趣些,想像這遊戲變成一個大型多人遊玩的網路遊戲,MoveManagement在玩家的電腦進行處理,但PlayerManagement由伺服器進行處理,PlayerManagement為所有連接過來的MoveManagement元件提供一個微服務API,下圖中,我們為該遊戲畫了一個簡單版的設計圖,現實中的Network元件通常會比圖中更複雜一些,從圖中我們可以看到MoveManagement與PlayerManagement之間存在一個完整的系統架構邊界。
總結
這整章究竟要表達什麼呢? 為什麼我們要用一個如此簡單的遊戲來實作這麼瘋狂的架構呢?
其實我們想說的是,處處都存在架構的邊界,作為一個架構師,應該小心辨識何時需要用它們,但同時也要意識到,充分實作這些架構,其代價會非常高,過度的工程設計通常會比不足的工程設計還要來得糟糕,但另外一方面,如果我們發現某個位置確實需要一個架構邊界但卻又沒事先設計的時候,事後再添加這些邊界其成本和風險往往是很高的。
架構的邊界不是一次性的決定,在專案的開始階段,千萬不要草率的決定要在哪實作邊界或要忽略哪個邊界,你必須觀察,這是一個持續的過程,隨著系統發展或許有些地方就會需要(或不需要)邊界(也需考量成本),總之,這真的需要好眼力。