來,你現在是一個軟體設計師,請問你想用什麼方式來體現你的軟體設計呢? 你有什麼更好方式表達來讓你的同事理解你的設計進一步跟你一起協作呢? 我認為在軟體設計的溝通上使用圖形化的工具是相當好的辦法。UML 裡的類別圖(Class Diagram)就是一個很好表達軟體架構設計的工具,要畫好類別圖會需要用到 6 種關係線來定義類別間的關係與如何實作,也是我們這次要關注的主題。
UML 的類別圖是軟體設計視覺化的一種體現
在網路上查找可以發現有很多類別圖的 6 種關係的說明與示例,通常不太容易難取得共鳴。主要有兩個原因:
這裡需要先插個話,補充一點 OOP 的知識。在 OOP 裡面類別(Class)是成員屬性(Member)與函數方法(Operation)的集合,按照其內容物的差異可分為三種:
其他的 OOP 的觀念有機會再說,這裡你需要先記得有類別(Class)跟介面(nterface) 這兩種就好。
類別有三種:一班類別、抽象類別、抽象介面
我們繼續回到類別關係上,在類別圖裡定義的 6 種關係線分別是:繼承(Inheritance) ,實現 (Realization),合成 (Composition),聚合 (Aggregation),相依 (Dependency) 以及 關聯 (Association)。
繼承關係套用程式書籍的說法叫做「父子」類別,負責繼承的是子類別,被繼承的是父類別,它是描述著兩個類別間具有「上下」關係。繼承概念就是「全盤接收」,子類別會接收父類別的所有屬性成員跟函數方法而且可以直接拿來用,所以子類別繼承後的預設的特性跟行為會跟父類別一致。
繼承關係重點是子類別還可以增加自己需要的成員屬性,利用擴充或複寫父類別的函數方法來跟父類別做出差異。就像我們與父母就是繼承關係,一樣也可以透過日後的學習成長有自己的特色與能力來跟父母產生差異,所謂「青出於藍更勝於藍」就是對繼承關係極佳註解。
在 OOP 的實作上,繼承關係可以是單一繼承或是多重繼承,多重繼承可以一次拿到多個父類別現有的能力,相對的設計跟除錯難度也會變得很高,例如當被繼承的兩個父類別都有一樣的函數方法,請問呼叫這個方法的時候,你覺得會叫到哪一個父類別的函數方法呢?
實作也是用來描述兩個類別的上下關係跟繼承類似,只要把的父類別換成是一個或多個抽象介面就可以了,所以實作關係也可以視為一種特殊的繼承關係。抽象介面的設計目的是把一組相關的函數方法(能力)定義出來綑綁在一起,負責實作介面的子類別必須按照介面的要求完全實作所有介面規定的函數方法。
實作的概念就是「有目的性的學習成長」,假設你也想成為程式設計師,那麼你就必須學會當程式設計師所需要的能力,例如至少會一種程式語言、要有編寫程式碼的與除錯的能力、要有代碼管理能力跟持續整合的能力等等。只要有完全實作出這組能力就算你是非本科系出身也可以是位合格的程式設計師了。
在 OOP 的實作上,類別可以實作一個到多個介面,在實作關係裡不管實作了多少介面,原則是所有介面有定義的函數方法都要全部實作出來。雖然實作多個介面來獲取能力不像多重繼承一次取得多個父類別的能力這麼方便,不過成功實作所有介面後,這種「能力插件」運作起來可以媲美多重繼承的效果。
合成跟聚合這兩種關係很像所以放在一起講,它們描述的是物件與部件的組合關係。在生活中你應該會觀察到大部分物件都是透過很多個部件組合,透過多個物件合作的方式運作的。
要區分合成或聚合的關鍵就是分析物件與它的部件兩者是否「密不可分」!只要你發現物件跟其部件分離後會出現兩個一起掛點的狀況,就表示兩者關係就是合成;相對的,物件跟部件分開後都活得好好的就叫做聚合。
最經典的合成關係莫過於我們的人體與體內的重要器官,像是心臟大腦… 一旦將它們從我們體內被分離出來,肯定兩個都活不了 ;善用「密不可分」的原則就可以很好的判斷兩個類別之間是合成還是聚合關係。
從 OOP 的實作上,合成或聚合的作用範圍都在成員屬性等級,物件類別會建立一個對應「部件」類別型態的成員屬性來保存這個部件。觀察「密不可分」的時候可以往物件解構那段程式去找找,如果你發現物件解構的時候也會把部件也解構掉,就表示兩者「密不可分」是為合成關係,否則就是聚合關係。
如果你是使用 C++ 這類需要手動清除記憶體的程式語言,那麼實作出正確的合成或聚合關係就很重要,因為該解構卻沒被解構的物件會造成記憶體洩漏,導致被佔用的記憶體無法釋放出來,隨時間增加你的程式會開始越來越難配置到記憶體空間,會變慢、變得不穩定,此時離當機也不遠了。
相依關係可以解釋為兩個類別間的「需要對方」關係,說到這邊你可能會想,難道合成或聚合裡的物件與部件兩者關係不也是相依關係嗎? 要區分這裡的相依關係的關鍵就是看那些被需要的物件出現的位置,以及這些物件是否有「用完就收」特性。
相依關係的作用範圍僅在函數方法等級,觀察重點在一個被需要的物件是否只出現在需求者物件的函數方法的參數列、程式碼區塊裡面內或是函數方法的回傳值裡面?在來看用完就收,當這些函數方法執行完畢後,這些被需要的物件,不會被保留在需求者物件上。
相依在概念上跟我們日常生活中臨時需要或是租借關係很像,再回到軟體工程師的例子,白天在公司上班要發揮編程能力時「需要」一台電腦跟外接螢幕作為編成函數方法的輸入(參數)。等下班軟體工程師會歸還這些設備並不會把它們帶回家。
到了 OOP 的實作上,判斷相依關係的撇步只需要去觀察調用它的函數方法裡有沒有把傳進來的物件用一個成員屬性存起來即可,如果沒有就是相依,反之我們就要往前面說的合成或聚合關係去確認了。
最後一種是叫做關聯關係,只要求兩個類別有”互相認識”對方就可以了是一種最低限度的關係。所謂認識的方式就是把自己類別的某個屬性開放出去給另一個需要認識自己的類別裡面。
這種關聯概念很像我們去訂閱某個資訊或服務,就像前陣子我想在網路書店購買一本原文書結果缺貨,於是我用電子郵件跟網路書店產生關聯,我把電子郵件放給網路書店,請它在有庫存的時候通知我;關聯關係也可是雙向多重的,反過來我也可以自己去訂閱各家書店的書本庫存數寄到我的電子郵件裡,只要發現有庫存我就可以去訂書。
關聯關係是一種平等自由、雙向多重的關係。它讓類別之間可以即時了解對方的狀況,再由類別實體們自己決定是否要根據這個狀態的改變來行動。
在 OOP 的實作上,會把這種關聯性建立在類別變數上,類別變數的概念可以理解為屬於該類別的全域變數,只要這個全域變數有變,則所有繼承這個類別實體物件自然也都會「自動」收到通知,當物件收到到訊息的後會決定如何做出反應。
以上就是這次要跟你聊的在 UML 裡的類別圖裡會用到的 6 種類別間的關係,如果你是第一次聽到感覺好像很複雜不容易弄懂,這是學習過程中很正常的現象,不需要浪費時間在自我懷疑身上,只要你願意多接觸,多花點時間,稍微靜下心弄清楚了定義(判斷標準),遇到抽象的觀念就嘗試用自己身邊熟悉的事物來類比,轉換成自己的語言,相信你也很快就能掌握 6 條線,讓這些設計語言成為你軟體設計工作與團隊溝通的利器。