當我們從事軟體開發工作一段時間後,有些人會開始接觸軟體架構設計。由於每個軟體架構設計者的對問題的理解與知識經驗差異會導出不同的設計架構。
近期與同事的軟體設計案例經驗交流後,就很希望自己剛開始學習軟體架構設計的時候就有人能用實際的軟體架構設計經驗來帶我入門。儘管這次討論的軟體架構設計規模較小,亦足以作為起點來聊聊作為一個新的軟體架構設計者希望開始就知道的 5 件事。
軟體架構設計的產出是 SAS (Software Architecture Specification) 也就是軟體架構設計規格書,此設計文件乘載了架構設計者的理念、對問題的理解與如何解決問題的觀點。
有時候軟體架構設計過程可以很簡單,例如一人統包設計兼開發的小專案,架構設計甚至簡單至一張紙、一草圖足矣;如果架構設計者面對的是跨團隊、大型複雜的軟體專案時就需要透過軟體設計工具 (ex: UML),來產出對應設計文件與團隊成員溝通協作。
當架構設計者要跟開發團隊討論靜態設計架構時就會用靜態類別圖 (Class Diagram) 來表達軟體內部的類別物件們與它們之間的彼此關聯性;想要說明物件互動狀態時,會用時序圖 (Sequence Diagram) 以時間軸的角度來呈現軟體元件間的互動與資料流動狀態;其他像是案例圖 (Use Case)、流程圖 (Flow Chart) 也能用來協助與非軟體職能的角色 (ex: PM) 進行溝通。
對架構設計者而言,學會一套軟體架構設計工具是必備的手段,利用工具做好架構設計、達成團隊的溝通才是真正的目的。
我認為一個好的軟體架構設計必須超越程式語言的限制達到設計共通性,換句話說,負責開發實作的人拿到設計圖後不管使用哪一種程式語言都要能順利實作才行。
舉例,在使用靜態類別圖用設計軟體架構時,設計者最好是選用對物件導向有嚴謹定義的程式語言來思考設計,例如 C#, Java 。嚴謹的語法定義提供了必要的類別 class、抽象類別 abstract 、介面 interface 等設計元素, public, private, protected 等能見度關鍵字也讓設計者能對成員(Member)、方法(Method)進行控制做好類別封裝的工作。
不過這次用來設計的程式語言是 Python,其物件導向的語法定義沒有可以用來宣告「介面」或「抽象類別」等關鍵字(只能觀察 Member 是否有 @abstract 的 decorator),類別成員與方法能見度也預設只有 public 一種,因此僅用 Python 作為觀點的架構設計者很容易受限。
只能用「類別與繼承」的設計方式,就像只用一隻手打架完全無法發揮真正實力。
Java 跟 C# 都是單一繼承+介面的設計理念,當我們想要描述 public abstract class A extends B implements C, D (C,D 是 Interface)的設計時,很容易在靜態類別圖上實現,開發者可以很清楚看出抽象類別 A 繼承了 B 且必須實作兩個介面 C, D(UML 的類別跟介面的 Icon 長不一樣)。
沒有介面關鍵字的 Python 設計者就只能把介面改用一般類別,然後想辦法在靜態設計圖上採取多重繼承方式來呈現上述的關係(或許 Python 的 Duck Type 也是一種介面的方式吧),於是同樣一句話到了 Python 這邊就會變成 class A extends B, C, D,想當然耳開發者拿到這樣的設計圖跟本無法意會哪些是應該設計成抽象類別、哪個又該設計成介面。
要記住軟體架構設計(ex: 類別圖)是設計給人看的,要避免模糊的定義或使用複雜多重繼承設計,因為那只會讓合作的開發者們感到困惑,增加你的溝通成本降低團隊開發效率而已。做為一個軟體架構設計者,我的主張是能使用單一繼承就不要搞多重繼承,化簡為繁並不是我們該做的事。
多重繼承能做到的設計,單一繼承+介面可以做得更漂亮
當我們在學習物件導向的繼承概念時最常被拿來說的好處就是 Reuse,因為子類別繼承父類別後就可直接調用繼承父類別過來成員與方法 (假設都是 public),而達成所謂的 Reuse,似乎有意地想把繼承跟 Reuse 畫上等號。
如果繼續把這樣的觀念發揮到極致的話,就會為了實現一個複雜物件的功能而繼承多個父類別或實作了一堆介面進而生出一個具有龐大複雜繼承體系的怪物物件。雖然這種的設計方式還是能被實作出來,但設計邏輯上並不符合現實世界的設計方式,想要解決這類設計問題的方式就是用合成(或聚合)來取代繼承關係。
所謂的「符合現實世界的設計」是指獨立模組功能、整合由小而大,整體通過整合不同的模組介面控制來實現我們想要的功能。以手機的拍照功能為例,手機實現照相功能並不是通過「繼承」相機來獲取照相功能的,不信的話可以把手機拆開觀察其內部(網路找圖,別真的自己拆手機),手機的照相功能是「合成」了一個或多個相機模組來實現的;換句話說,我們看的到的手機,就是一個合成了多個功能模組的集合體,所以「獨立模組功能、整合從小而大、整體通過模組介面整合」才是我們符合現實世界的設計邏輯。
對,使用合成取代繼承除了符合設計邏輯外,功能獨立的模組也代表變化的部分封裝的更好;整體可以通過介面控制的模組也代表耦合性低,才是我們該做的事。
在軟體架構設計上力求「高內聚、低耦合」
我認為一個好的架構設計者除了具備上述良好的設計邏輯的觀念外,也應該仔細思考共同模組的設計策略。可以採用 top-down 從全局觀察的介面設計或是 bottom-up 先建構物件後再來進一步抽象化最終達到介面設計。除非我們一開始入行很幸運就落腳在一個組織規模夠大、角色分工明確的軟體團隊裡接受專業的架構設計訓練,否則我想大部分的人也都應該都是從 bottom-up 的方式逐步修練、累積設計經驗值成長起來的。
Bottom-up 的策略很簡單直覺,通常是先把需要的類別物件按照專案要求先個別設計出來,等專案活動告一段落後再回頭重構。重構過程中不斷整理提取抽象概念逐步改善,等累積更多開發設計經驗後,再嘗試進一步提取成介面為主的架構設計。Bottom-up 過程需要多次的迭代過程看似緩慢,但對於剛入門或經驗不足架構設計者我反而覺得這是很好的訓練,在每一次重構過程中培養自己抽象化思考的能力,這也是大部分自學架構設計者的自然成長模式。
相較之下經驗豐富的架構設計者會更傾向於採用 top-down 的方式,不急著著手設計物件細節,而是會先做各種調查活動想辦法掌握全局思維,因為他們知道使用介面為主的設計會更優秀。當我們想要設計通訊協議的模組給不同的產品使用時,有經驗設計者會先觀察此模組要提供給哪些產品用,如果一個產品支持多種協議,那麼不同的協議間又有甚麼行為的共通性,透過問各種問題逐步分析思考應該定義哪些的介面來規劃通訊模組與產品整合所需介面。
例如在經過觀察了多種產品的各種通訊協議後發現所有通訊最基礎的能力就是讀跟寫,就可以定義一個 IProtocol 介面來要求具備通訊介面物件都要實作讀跟寫兩種能力;對常見的 Modbus 通訊模組來說就是可以從 IProtocol 衍生的 IModbus 介面來規範 read write register, coil 等能力。
介面設計可以讓物件的控制行為保持一致性與具備可隨意替換的彈性。
只用案例來看架構設計難免以管窺天,卻是新手軟體設計者一窺堂奧的開始。對於想從事軟體架構設計工作的人來說,先有耐心地完備該有的知識理論絕對必要的,現在的資訊發達,想學習軟體架構設計的知識跟工具等資源幾乎都能免費取得,不用擔心知識匱乏,反而是有實踐理論的實戰案例才是難得的。如果你是想入門或正修行學習軟體架構設計的人,我會建議一定要睜大你的眼睛去尋找你團隊裡最有實戰經驗的高手們學習,請他們喝杯咖啡、分享你的設計,多聽聽聽他們的故事你會看見不同的世界。