感受
在剛接觸軟體開發工作的時候,每次接到新需求,在分析需求後的第一件事情,就是火急火燎的開啟資料庫(DBMS),開始進行資料表的建立工作。然而這種方式,總是會讓我在編碼過程中出現實體類設計疏漏的地方,導致我在寫業務程式碼時,還回頭去反覆的修改資料表和實體類。為了規避這樣的情況,我學習期間發現了UML中關於類圖的知識點,它讓我知道,作為編碼者在分析需求後,做的第一件最基本的事情應該是進行物件導向分析,然後使用UML繪製類圖的方式進行物件導向的設計。在類圖繪製完之後,使用類圖與組員溝通設計思想,分析設計的可行性,在專案組一致達成共識後才進入後面的動手環節。
以上這種,通過物件導向分析和設計來繪製類圖的工作習慣,我一直延續至今。因為,它不僅能保證軟體構建的穩定性,還能提升我們物件導向的思想和實踐能力。在實際中,極少數的情況下,公司會聘用專門的設計人員為你提供設計方案,更多的情況是,程式設計師要擔任設計和編碼的綜合性工作,所以我認為掌握UML類圖,是一名程式設計師的技能標配。
三個層次
在標準的軟體工程建模當中,類圖實際上根據三個層次劃分為了三種型別的類圖,根據使用順序分別為:概念層類圖、說明層類圖和實現層類圖。概念層用於業務建模階段,著重於對問題領域的概念化理解;說明層用於概念模型階段,主要考察類的互動涉及哪些介面;實現層用於設計階段,主要考慮類在程式碼技術層面的實現細節。本文主要主要以實現層的類圖為主,因為實現層的類圖是最常用的,並且它是直接影響到我們實際的編碼工作的,下面我會針對它涉及的繪製方式、類之間的關係展開詳細講解。
UML類圖的基本語法是很簡單的,可能懂點程式設計的人在不繫統學習的情況下,藉助繪圖工具都可以繪製出來。但在實際的業務需求中,充斥著各種晦澀的業務概念、事物,要從其中準確無誤的提煉出有利於業務系統的類,並非一件簡單的事情。
對於類的識別,並沒有很具體的步驟、公式進行照搬硬套,往往只能通過自身的經驗和物件導向的造詣去識別類。並且識別類往往也不是一蹴而就的,還要結合類與類之間的關係、業務的使用場景,反覆推敲,才能逐步得到合適的型別。對此我只能提供一些概念性的經驗心得,讀者可以選擇性的參考,並不作為一個標準。
類的識別很大程度上需要依靠「邊界」,這是一個複雜的概念,你可以簡單理解它相當於一個範圍,設定邊界可以讓我們知道能做什麼事情,和不能做什麼事情。並且邊界的設定會決定我們看待事物的視角和抽象事物的層次。對於類的識別而言,其邊界可參考當前的系統的目標、業務場景等,有了清晰的邊界,可以縮小類的識別範圍,不在是天馬行空,毫無根據。
如果不通過邊界確定一個角度,那麼對於同一事物,通過不同的角度會提煉出不同的型別。就拿我們自身舉例,從職業的角度來看我們則是程式設計師,從國家的角度來看我們則是中國人,從動物的角度來看我們則是人類。所以我們必須要通過邊界來確定一個角度,從而清晰的分析獲取有利於業務系統的型別。
例如,你需要在一個網課教育系統中,分析上課的場景中會有哪些型別。如果你不考慮邊界(網課教育系統中的上課場景),那麼你可能天馬行空的分析出:男人、女人這些型別。這樣分析出的型別和屬性顯然對系統毫無意義,也無法為業務提供價值。如果你考慮到了邊界(網課教育系統中的上課場景),那麼你分析出的型別必然是在這個邊界內有利於業務的:老師、學生。
對於分析類中的成員(屬性、操作)也可以利用邊界來分析。還是以上面的網課教育系統為例,如果不考慮邊界,很可能會對老師類和學生類分析出:體重、身高、髮量等無意義的屬性。只有你充分考慮邊界,你就會注重系統的目標、業務的場景,分析出對業務有價值的屬性,例如學生類的選修課程、老師類的教齡等。
如果你對邊界的概念還是比較模糊,那麼你可以在識別類的時候,嘗試將當前的系統目標、業務場景看作一個邊界,從而選擇合適的角度,去提煉出對業務系統有價值的型別。
可見性主要用於標識類圖中的屬性和操作,通過設定不同的可見性決定外界對其的存取程度,和程式語言中的存取修飾符同理。UML規範定義了4種可見性,如下表所示。
類在UML類圖中的形狀是一個矩形的方框,在方框中被分為三段區域,上段主要是標識類的名稱,中段主要包含類的屬性(特徵),下段主要是包含類的是操作(行為)。表示一個類時,三段區域的設定並不是必須的,可以只在矩形方框中寫一個類名,也可以只寫類名和屬性,或者是類名和操作。
下面將使用C#程式語言編寫出:普通型別、抽象類、介面。然後體現出它們在類圖中的表現形式。
普通類 |
|
|
|
抽象類(類名和抽象方法名都是斜體) |
|
|
|
介面(名稱上方加<<interface>>) |
|
|
概述
我們以物件導向的思想去分析業務時,其中最基本的是,要弄清楚完成這個業務需要哪些物件。但是往往只分析出物件還遠遠不夠,因為業務物件之間是相互獨立的。物件之間必須建立某種連結,促使它們相互共同作業通訊,才能實現業務目標。而這其中用於連結物件的關係,就稱之為關聯。換句話說,只要兩個物件之間存在關聯,那麼就意味著物件可以與它關聯物件進行通訊,獲取對方的資料進行訊息傳遞。
結構化
關聯定義了類之間的一種結構化關係,是一種天然存在的關係。(好比如人出生就有擁有一個國家和一對父母)通常在程式碼中,關聯關係體現為類的屬性,如A關聯B,那麼B的物件會作為A物件的某個屬性。在例如在運用ORM框架的程式碼中,類的關聯物件通常定義為一個「導航屬性」,可以通過這個導航屬性獲取到關聯物件的資料。在例如資料庫中,表的關聯物件通常體現為一個「外來鍵」屬性,表的某行資料可以通過這個外來鍵屬性獲取到關聯表的資料。不管是導航屬性或是外來鍵,它們都是靜態的、天然存在的結構。
方向
預設的關聯關係是一條不帶箭頭的直線表示的,這代表著兩個關聯的類「知道」雙方的存在,並可以互相參照。在少數情況下,當兩個類之間只需要單方向連結來獲取訊息時,就需要標識箭頭指向被連結的一方。在實際中,我們不必太過於究竟箭頭的方向,大多數情況下,關聯關係一般不強調關聯的方向。
多重性
關聯關係最明顯的特徵就是具有多重性,其意思是一個物件可能通過關聯關係連結到多個物件上。例如張三是員工類的物件,那麼張三很可能會通過與「工作任務類」之間的關聯,連結獲取到張三在「工作任務類」中存在的多個工作任務物件(設計XX、開發XX等),這當中物件通過關聯連結到資料量的「多少」即為多重性的體現。常見的多重性包括:一對一關聯、一對多關聯、多對多關聯等,也可以是任意數量的多重性關聯,如*對*關聯(*代表任意數)。
多重性 |
例子 |
圖例 |
一對一 |
在某個教室中,一個學生只會指定一個座位,一個座位也只會安排一個學生。因此學生和座位之間是一對一的關聯關係。 |
|
一對多 |
在現實生活中,一個人可以選擇購買合法上牌的多輛汽車,而一輛合法上牌的車只屬於一個人。在這個場景中,人和車輛之間就屬於一對多的關聯關係。 |
|
多對多 |
在學校中,一名學生會學校多門課程(語數外),而一門課程(語文)會有多名學生學習。在這個場景中,學生和課程之間屬於多對多的關聯關係。 |
|
讀圖檢查
在分析關聯關係的多重性是否合理時,可以通過「讀圖檢查法」來進行關聯關係的準確性判斷。你可以分別從左到右、從右到左來讀圖,看看有沒有不合理的地方。我們使用上面多重性表格中人和車的關係為例,從左到右讀:一個人對應零到多個車。從右到左讀:一輛車對應一個人,而不能讀成:多輛車對應一個人。注意由「多」的一邊往另外一邊讀時,仍然是一個什麼對應多少個什麼,無論你從哪邊開始讀起,都是以「一個.....」開頭。
在分析出兩個類的關聯關係之後,兩個關聯的類中可能還存在一種整體和部分的含義,即存在整體包含部分的現象。對於這種,存在整體和部分含義的關聯關係可以進一步細化,表示成聚合關係。聚合關係可以看作,是在普通關聯的基礎上細化的一種特殊關聯關係。除了擁有關聯關係所有的基本特徵外,其中一個類描述了一個較大的事物(整體),另一個類代表較小的事物(部分),較小的事物可以構成一個較大的事物。
對於聚合關係的識別,可以在已有的關聯關係基礎上,通過分析兩個類之間是否存在:「整體由部分構成」、「部分是整體的一部分」等整體和部分的語意來完成。例如對於一個OA辦公系統來說,其中部門和員工之間的關聯關係就存在著整體和部分的含義。員工是部門的一部分,部門由員工構成。聚合關係是用一條帶空心菱形箭頭的直線表示,空心菱形箭頭指向的一端表示「整體」,反方向是「部分」。範例的聚合關係如下圖所示。
組合關係是在聚合關係基礎之上延申的一種關聯關係,還可以將它看作是聚合關係的變體,或者是對聚合關係的進一步強調。因此組合關係具有關聯、聚合的所有特徵。在分析出聚合關係之後,還可以對針對整體和部分做進一步的分析:兩者之間除了整體擁有部分的語意之外,兩者之間是否屬於「強依賴」;並且整體和部分的生命週期是一致的。如果存在以上的特點,那麼可以將其表示為組合關係。
「強依賴」和一致的生命週期意味著:整體擁有部分的同時,部分不能脫離整體而存在;當整體不存在時,部分也沒有存在的意義。對於組合關係中的整體和部分之間的關係特點,我們可以用一則成語來形象的描述:「皮之不存,毛將焉附」。在這種特點上,它和聚合關係恰恰相反,聚合關係即使整體不存在了,部分也依然存在。如果你認為聚合和組合比較容易混淆,那麼你可以將聚合看成「弱包含關係」,組合可以看成「強包含關係」,以此來區分兩者之間的差異。
基於組合關係中整體和部分的「強依賴」現象,因此在圖中表示該關係的箭頭,是由一條實心菱形箭頭的直線表示,實心菱形箭頭指向的一端表示「整體」,反方向是「部分」。範例的組合關係如下圖所示。
概述
依賴關係是一種側重於「行為」的使用關係,表示某個物件在某個場景下產生的行為,需要使用另外一個物件提供的服務來完成。這也意味著,被使用物件的變化可能會影響到使用物件。依賴關係的分析要結合特定的場景和相應的行為,這一點可表面它屬於一種臨時性的關係,它通常在行為執行期產生,並且隨著執行場景的不同,依賴的物件也會發生變化。
臨時性
例如人和汽車這兩個物件,如果執行場景是讓汽車執行在馬路上,那麼汽車的執行則需要依賴於人的駕駛;如果場景變為人乘坐汽車去上班,那就變成人上班通勤依賴於汽車的送達。可見,它並不像關聯關係那樣是一種天然的結構化關係,依賴關係是短暫的,它會隨著不同場景的變化而變化的,並且依賴關係是基於場景下的行為所產生的,使用場景結束後,依賴關係也會暫時消失。如人和菜刀這兩個物件,靜態時它們沒有關係,但在廚房切菜的場景裡,人切菜的行為就依賴於菜刀;脫離了這個切菜的場景,人就暫時不需要菜刀了。
運用
依賴關係的參照通常在程式碼裡體現為:類構造方法的引數、方法的引數。在分析時,如果發現A物件需要儲存B物件的範例,但A物件的類中對B物件沒有操作,B發生修改後,A不會發生變化,僅僅是A「知道」B物件,那麼可以將其定義為關聯關係。在分析時,如果發現A物件需要在某個業務場景的方法中,使用入參物件B的屬性或方法,那麼可以將其定義為依賴關係,這同時也意味著,B的修改會導致A發生修改(A依賴於B)。依賴關係在圖中用一條帶箭頭的虛線表示,箭頭指向被依賴的物件。
泛化關係表明,一個類可以共用另外一個或多個類的結構和行為。為了實現泛化關係,我們引入了繼承機制,一個子類可以繼承一個或多個父類別,子類會繼承父類別的屬性、操作和關係,因此我們通常也將泛化稱為繼承。此外,子類還可以根據自己的需要新增額外的屬性、操作或關係,還可以對父類別已有的操作進行重新定義。其中,繼承一個父類別為單一繼承,繼承多個父類別為多重繼承。在實際的系統應用中,我們大多數採用單一繼承,因為多重繼承會存在一些隱患問題,並且主流的程式語言(Java、C#)都不支援多重繼承。
泛化關係除了實現複用性,更深層次的目的是達到父類別替代子類的可替換性,從而實現多型處理。另外,在分析出泛化關係後,可以通過描述類之間是否存在[is a 關係]或者[kind of 關係]的語意來驗證。具體來說,就是「子類是父類別」(貓是動物),或「子類是父類別的一種」(貓是動物的一種)。
泛化關係是用一條帶空心箭頭的直線表示。如下圖展示了學生管理系統的一種泛化關係,其中代表子類(畢業生類和新生類)都從父類別(學生類)繼承,它們繼承了父類別全部屬性和操作。此外,子類也會繼承父類別中的關係,因此畢業生類和新生類於賬戶類也有聚合關係。
UML類圖的學習並不是一蹴而就的,也不能指望看了幾篇教學就認為自己會了。在學習初期階段先要保證自己能夠讀懂類圖,然後可以根據已有的業務分析結果「照葫蘆畫瓢」的繪製出來,最後關鍵的還是要在於通過實踐,根據具體業務發散出物件導向思想,並能將這個思想通過適當的方式在類圖中簡單明瞭的體現出來。