深入淺出UML類圖

世紀緣發表於2017-04-13

在UML 2.0的13種圖形中,類圖是使用頻率最高的UML圖之一。Martin Fowler在其著作《UML Distilled: A Brief Guide to the Standard Object Modeling Language, Third Edition》(《UML精粹:標準物件建模語言簡明指南(第3版)》)中有這麼一段:“If someone were to come up to you in a dark alley and say, 'Psst, wanna see a UML diagram?' that diagram would probably be a class diagram. The majority of UML diagrams I see are class diagrams.”(“如果有人在黑暗的小巷中向你走來並對你說:‘嘿,想不想看一張UML圖?’那麼這張圖很有可能就是一張類圖,我所見過的大部分的UML圖都是類圖”),由此可見類圖的重要性。

類圖用於描述系統中所包含的類以及它們之間的相互關係,幫助人們簡化對系統的理解,它是系統分析和設計階段的重要產物,也是系統編碼和測試的重要模型依據。

1. 類

類(Class)封裝了資料和行為,是物件導向的重要組成部分,它是具有相同屬性、操作、關係的物件集合的總稱。在系統中,每個類都具有一定的職責,職責指的是類要完成什麼樣的功能,要承擔什麼樣的義務。一個類可以有多種職責,設計得好的類一般只有一種職責。在定義類的時候,將類的職責分解成為類的屬性和操作(即方法)。類的屬性即類的資料職責,類的操作即類的行為職責。設計類是物件導向設計中最重要的組成部分,也是最複雜和最耗時的部分。

在軟體系統執行時,類將被例項化成物件(Object),物件對應於某個具體的事物,是類的例項(Instance)。

類圖(Class Diagram)使用出現在系統中的不同類來描述系統的靜態結構,它用來描述不同的類以及它們之間的關係。

在系統分析與設計階段,類通常可以分為三種,分別是實體類(Entity Class)、控制類(Control Class)和邊界類(Boundary Class),下面對這三種類加以簡要說明:

(1) 實體類:實體類對應系統需求中的每個實體,它們通常需要儲存在永久儲存體中,一般使用資料庫表或檔案來記錄,實體類既包括儲存和傳遞資料的類,還包括運算元據的類。實體類來源於需求說明中的名詞,如學生、商品等。

(2) 控制類:控制類用於體現應用程式的執行邏輯,提供相應的業務操作,將控制類抽象出來可以降低介面和資料庫之間的耦合度。控制類一般是由動賓結構的短語(動詞+名詞)轉化來的名詞,如增加商品對應有一個商品增加類,註冊對應有一個使用者註冊類等

(3) 邊界類:邊界類用於對外部使用者與系統之間的互動物件進行抽象,主要包括介面類,如對話方塊、視窗、選單等。

在物件導向分析和設計的初級階段,通常首先識別出實體類,繪製初始類圖,此時的類圖也可稱為領域模型,包括實體類及其它們之間的相互關係。

2. 類的UML圖示

在UML中,類使用包含類名、屬性和操作且帶有分隔線的長方形來表示,如定義一個Employee類,它包含屬性name、age和email,以及操作modifyInfo(),在UML類圖中該類如圖1所示:

圖1 類的UML圖示

圖1對應的Java程式碼片段如下:

public class Employee {
	private String name;
	private int age;
	private String email;
	
	public void modifyInfo() {
		......
	}
}

在UML類圖中,類一般由三部分組成:

(1) 第一部分是類名:每個類都必須有一個名字,類名是一個字串。

(2) 第二部分是類的屬性(Attributes):屬性是指類的性質,即類的成員變數。一個類可以有任意多個屬性,也可以沒有屬性

UML規定屬性的表示方式為:

可見性 名稱:型別 [ = 預設值 ]

其中:

  • “可見性”表示該屬性對於類外的元素而言是否可見,包括公有(public)、私有(private)和受保護(protected)三種,在類圖中分別用符號+、-和#表示。
  • “名稱”表示屬性名,用一個字串表示。
  • “型別”表示屬性的資料型別,可以是基本資料型別,也可以是使用者自定義型別。
  • “預設值”是一個可選項,即屬性的初始值。

(3) 第三部分是類的操作(Operations):操作是類的任意一個例項物件都可以使用的行為,是類的成員方法。

UML規定操作的表示方式為:

可見性 名稱(引數列表) [ : 返回型別]

其中:

  • “可見性”的定義與屬性的可見性定義相同。
  • “名稱”即方法名,用一個字串表示。
  • “引數列表”表示方法的引數,其語法與屬性的定義相似,引數個數是任意的,多個引數之間用逗號“,”隔開。
  • “返回型別”是一個可選項,表示方法的返回值型別,依賴於具體的程式語言,可以是基本資料型別,也可以是使用者自定義型別,還可以是空型別(void),如果是構造方法,則無返回型別。

在類圖2中,操作method1的可見性為public(+),帶入了一個Object型別的引數par,返回值為空(void);操作method2的可見性為protected(#),無引數,返回值為String型別;操作method3的可見性為private(-),包含兩個引數,其中一個引數為int型別,另一個為int[]型別,返回值為int型別。

圖2 類圖操作說明示意圖

由於在Java語言中允許出現內部類,因此可能會出現包含四個部分的類圖,如圖3所示:

圖3 包含內部類的類圖

類與類之間的關係(1)

在軟體系統中,類並不是孤立存在的,類與類之間存在各種關係,對於不同型別的關係,UML提供了不同的表示方式。

1. 關聯關係

關聯(Association)關係是類與類之間最常用的一種關係,它是一種結構化關係,用於表示一類物件與另一類物件之間有聯絡,如汽車和輪胎、師傅和徒弟、班級和學生等等。在UML類圖中,用實線連線有關聯關係的物件所對應的類,在使用Java、C#和C++等程式語言實現關聯關係時,通常將一個類的物件作為另一個類的成員變數。在使用類圖表示關聯關係時可以在關聯線上標註角色名,一般使用一個表示兩者之間關係的動詞或者名詞表示角色名(有時該名詞為例項物件名),關係的兩端代表兩種不同的角色,因此在一個關聯關係中可以包含兩個角色名,角色名不是必須的,可以根據需要增加,其目的是使類之間的關係更加明確。

如在一個登入介面類LoginForm中包含一個JButton型別的註冊按鈕loginButton,它們之間可以表示為關聯關係,程式碼實現時可以在LoginForm中定義一個名為loginButton的屬性物件,其型別為JButton。如圖1所示:

圖1 關聯關係例項

圖1對應的Java程式碼片段如下:

public class LoginForm {
private JButton loginButton; //定義為成員變數
……
}

public class JButton {
    ……
}

在UML中,關聯關係通常又包含如下幾種形式:

(1) 雙向關聯

預設情況下,關聯是雙向的。例如:顧客(Customer)購買商品(Product)並擁有商品,反之,賣出的商品總有某個顧客與之相關聯。因此,Customer類和Product類之間具有雙向關聯關係,如圖2所示:

圖2 雙向關聯例項

圖2對應的Java程式碼片段如下:

public class Customer {
private Product[] products;
……
}

public class Product {
private Customer customer;
……
}

(2) 單向關聯

類的關聯關係也可以是單向的,單向關聯用帶箭頭的實線表示。例如:顧客(Customer)擁有地址(Address),則Customer類與Address類具有單向關聯關係,如圖3所示:

圖3 單向關聯例項

圖3對應的Java程式碼片段如下:

public class Customer {
private Address address;
……
}

public class Address {
……
}

(3) 自關聯

在系統中可能會存在一些類的屬性物件型別為該類本身,這種特殊的關聯關係稱為自關聯。例如:一個節點類(Node)的成員又是節點Node型別的物件,如圖4所示:

圖4 自關聯例項

圖4對應的Java程式碼片段如下:

public class Node {
private Node subNode;
……
}

(4) 多重性關聯

多重性關聯關係又稱為重數性(Multiplicity)關聯關係,表示兩個關聯物件在數量上的對應關係。在UML中,物件之間的多重性可以直接在關聯直線上用一個數字或一個數字範圍表示。

物件之間可以存在多種多重性關聯關係,常見的多重性表示方式如表1所示:

表1 多重性表示方式列表

表示方式
多重性說明
1..1
表示另一個類的一個物件只與該類的一個物件有關係
0..*
表示另一個類的一個物件與該類的零個或多個物件有關係
1..*
表示另一個類的一個物件與該類的一個或多個物件有關係
0..1
表示另一個類的一個物件沒有或只與該類的一個物件有關係
m..n
表示另一個類的一個物件與該類最少m,最多n個物件有關係 (m≤n)

例如:一個介面(Form)可以擁有零個或多個按鈕(Button),但是一個按鈕只能屬於一個介面,因此,一個Form類的物件可以與零個或多個Button類的物件相關聯,但一個Button類的物件只能與一個Form類的物件關聯,如圖5所示:

圖5 多重性關聯例項

圖5對應的Java程式碼片段如下:

public class Form {
private Button[] buttons; //定義一個集合物件
……
}

public class Button {
……
}

(5) 聚合關係

聚合(Aggregation)關係表示整體與部分的關係。在聚合關係中,成員物件是整體物件的一部分,但是成員物件可以脫離整體物件獨立存在。在UML中,聚合關係用帶空心菱形的直線表示。例如:汽車發動機(Engine)是汽車(Car)的組成部分,但是汽車發動機可以獨立存在,因此,汽車和發動機是聚合關係,如圖6所示:

圖6 聚合關係例項

在程式碼實現聚合關係時,成員物件通常作為構造方法、Setter方法或業務方法的引數注入到整體物件中,圖6對應的Java程式碼片段如下:

public class Car {
	private Engine engine;

    //構造注入
	public Car(Engine engine) {
		this.engine = engine;
	}
    
    //設值注入
public void setEngine(Engine engine) {
    this.engine = engine;
}
……
}

public class Engine {
	……
} 

(6) 組合關係

組合(Composition)關係也表示類之間整體和部分的關係,但是在組合關係中整體物件可以控制成員物件的生命週期,一旦整體物件不存在,成員物件也將不存在,成員物件與整體物件之間具有同生共死的關係。在UML中,組合關係用帶實心菱形的直線表示。例如:人的頭(Head)與嘴巴(Mouth),嘴巴是頭的組成部分之一,而且如果頭沒了,嘴巴也就沒了,因此頭和嘴巴是組合關係,如圖7所示:

圖7 組合關係例項

在程式碼實現組合關係時,通常在整體類的構造方法中直接例項化成員類,圖7對應的Java程式碼片段如下:

public class Head {
	private Mouth mouth;

	public Head() {
		mouth = new Mouth(); //例項化成員類
	}
……
}

public class Mouth {
	……
} 

類與類之間的關係(2)

2. 依賴關係

依賴(Dependency)關係是一種使用關係,特定事物的改變有可能會影響到使用該事物的其他事物,在需要表示一個事物使用另一個事物時使用依賴關係。大多數情況下,依賴關係體現在某個類的方法使用另一個類的物件作為引數。在UML中,依賴關係用帶箭頭的虛線表示,由依賴的一方指向被依賴的一方。例如:駕駛員開車,在Driver類的drive()方法中將Car型別的物件car作為一個引數傳遞,以便在drive()方法中能夠呼叫car的move()方法,且駕駛員的drive()方法依賴車的move()方法,因此類Driver依賴類Car,如圖1所示:

圖1 依賴關係例項

在系統實施階段,依賴關係通常通過三種方式來實現,第一種也是最常用的一種方式是如圖1所示的將一個類的物件作為另一個類中方法的引數,第二種方式是在一個類的方法中將另一個類的物件作為其區域性變數,第三種方式是在一個類的方法中呼叫另一個類的靜態方法。圖1對應的Java程式碼片段如下:

public class Driver {
	public void drive(Car car) {
		car.move();
	}
    ……
}

public class Car {
	public void move() {
		......
	}
    ……
}  

3. 泛化關係

泛化(Generalization)關係也就是繼承關係,用於描述父類與子類之間的關係,父類又稱作基類或超類,子類又稱作派生類。在UML中,泛化關係用帶空心三角形的直線來表示。在程式碼實現時,我們使用物件導向的繼承機制來實現泛化關係,如在Java語言中使用extends關鍵字、在C++/C#中使用冒號“:”來實現。例如:Student類和Teacher類都是Person類的子類,Student類和Teacher類繼承了Person類的屬性和方法,Person類的屬性包含姓名(name)和年齡(age),每一個Student和Teacher也都具有這兩個屬性,另外Student類增加了屬性學號(studentNo),Teacher類增加了屬性教師編號(teacherNo),Person類的方法包括行走move()和說話say(),Student類和Teacher類繼承了這兩個方法,而且Student類還新增方法study(),Teacher類還新增方法teach()。如圖2所示:

圖2 泛化關係例項

圖2對應的Java程式碼片段如下:

//父類
public class Person {
protected String name;
protected int age;

public void move() {
        ……
}

    public void say() {
    ……
    }
}

//子類
public class Student extends Person {
private String studentNo;

public void study() {
    ……
    }
}

//子類
public class Teacher extends Person {
private String teacherNo;

public void teach() {
    ……
    }
}

4. 介面與實現關係

在很多物件導向語言中都引入了介面的概念,如Java、C#等,在介面中,通常沒有屬性,而且所有的操作都是抽象的,只有操作的宣告,沒有操作的實現。UML中用與類的表示法類似的方式表示介面,如圖3所示:

圖3 介面的UML圖示

介面之間也可以有與類之間關係類似的繼承關係和依賴關係,但是介面和類之間還存在一種實現(Realization)關係,在這種關係中,類實現了介面,類中的操作實現了介面中所宣告的操作。在UML中,類與介面之間的實現關係用帶空心三角形的虛線來表示。例如:定義了一個交通工具介面Vehicle,包含一個抽象操作move(),在類Ship和類Car中都實現了該move()操作,不過具體的實現細節將會不一樣,如圖4所示:

圖4 實現關係例項

實現關係在程式設計實現時,不同的面嚮物件語言也提供了不同的語法,如在Java語言中使用implements關鍵字,而在C++/C#中使用冒號“:”來實現。圖4對應的Java程式碼片段如下:

public interface Vehicle {
public void move();
}

public class Ship implements Vehicle {
public void move() {
    ……
    }
}

public class Car implements Vehicle {
public void move() {
    ……
    }
}

例項分析1——登入模組

某基於C/S的即時聊天系統登入模組功能描述如下:

使用者通過登入介面(LoginForm)輸入賬號和密碼,系統將輸入的賬號和密碼與儲存在資料庫(User)表中的使用者資訊進行比較,驗證使用者輸入是否正確,如果輸入正確則進入主介面(MainForm),否則提示“輸入錯誤”。

根據以上描述繪製初始類圖。

參考解決方案:

參考類圖如下:

考慮到系統擴充套件性,在本例項中引入了抽象資料訪問介面IUserDAO,再將具體資料訪問物件注入到業務邏輯物件中,可通過配置檔案(如XML檔案)等方式來實現,將具體的資料訪問類類名儲存在配置檔案中,如果需要更換新的具體資料訪問物件,只需修改配置檔案即可,原有程式程式碼無須做任何修改。

類說明:

類 名
說 明
LoginForm 登入視窗,省略介面元件和按鈕事件處理方法(邊界類)
LoginBO 登入業務邏輯類,封裝實現登入功能的業務邏輯(控制類)
IUserDAO 抽象資料訪問類介面,宣告對User表的資料操作方法,省略除查詢外的其他方法(實體類)
UserDAO 具體資料訪問類,實現對User表的資料操作方法,省略除查詢外的其他方法(實體類)
MainForm 主視窗(邊界類)

方法說明:

方法名
說 明
LoginForm類的LoginForm()方法 LoginForm建構函式,初始化例項成員
LoginForm類的validate()方法 介面類的驗證方法,通過呼叫業務邏輯類LoginBO的validate()方法實現對使用者輸入資訊的驗證
LoginBO類的validate()方法 業務邏輯類的驗證方法,通過呼叫資料訪問類的findUserByAccAndPwd()方法驗證使用者輸入資訊的合法性
LoginBO類的setIUserDAO()方法 Setter方法,在業務邏輯物件中注入資料訪問物件(注意:此處針對抽象資料訪問類程式設計
IUserDAO介面的findUserByAccAndPwd()方法 業務方法宣告,通過使用者賬號和密碼在資料庫中查詢使用者資訊,判斷該使用者身份的合法性
UserDAO類的findUserByAccAndPwd()方法 業務方法實現,實現在IUserDAO介面中宣告的資料訪問方法

例項分析2——註冊模組

某基於Java語言的C/S軟體需要提供註冊功能,該功能簡要描述如下:

使用者通過註冊介面(RegisterForm)輸入個人資訊,使用者點選“註冊”按鈕後將輸入的資訊通過一個封裝使用者輸入資料的物件(UserDTO)傳遞給運算元據庫的資料訪問類,為了提高系統的擴充套件性,針對不同的資料庫可能需要提供不同的資料訪問類,因此提供了資料訪問類介面,如IUserDAO,每一個具體資料訪問類都是某一個資料訪問類介面的實現類,如OracleUserDAO就是一個專門用於訪問Oracle資料庫的資料訪問類。

根據以上描述繪製類圖。為了簡化類圖,個人資訊僅包括賬號(userAccount)和密碼(userPassword),且介面類無需涉及介面細節元素。

參考解決方案:

在以上功能說明中,可以分析出該系統包括三個類和一個介面,這三個類分別是註冊介面類RegisterForm、使用者資料傳輸類UserDTO、Oracle使用者資料訪問類OracleUserDAO,介面是抽象使用者資料訪問介面IUserDAO。它們之間的關係如下:

(1) 在RegisterForm中需要使用UserDTO類傳輸資料且需要使用資料訪問類來運算元據庫,因此RegisterForm與UserDTO和IUserDAO之間存在關聯關係,在RegisterForm中可以直接例項化UserDTO,因此它們之間可以使用組合關聯。

(2) 由於資料庫型別需要靈活更換,因此在RegisterForm中不能直接例項化IUserDAO的子類,可以針對介面IUserDAO程式設計,再通過注入的方式傳入一個IUserDAO介面的子類物件(在本書後續章節中將學習如何具體實現),因此RegisterForm和IUserDAO之間具有聚合關聯關係。

(3) OracleUserDAO是實現了IUserDAO介面的子類,因此它們之間具有類與介面的實現關係。

(4) 在宣告IUserDAO介面的增加使用者資訊方法addUser()時,需要將在介面類中例項化的UserDTO物件作為引數傳遞進來,然後取出封裝在UserDTO物件中的資料插入資料庫,因此addUser()方法的函式原型可以定義為:public boolean addUser(UserDTO user),在IUserDAO的方法addUser()中將UserDTO型別的物件作為引數,故IUserDAO與UserDTO存在依賴關係。

通過以上分析,該例項參考類圖如圖1所示:

圖1 註冊功能參考類圖

注意:在繪製類圖或其他UML圖形時,可以通過註釋(Comment)來對圖中的符號或元素進行一些附加說明,如果需要詳細說明類圖中的某一方法的功能或者實現過程,可以使用如圖2所示表示方式:

圖2 類圖註釋例項

例項分析3——售票機控制程式

某運輸公司決定為新的售票機開發車票銷售的控制軟體。圖I給出了售票機的皮膚示意圖以及相關的控制部件。

圖I 售票機皮膚示意圖

售票機相關部件的作用如下所述:

(1) 目的地鍵盤用來輸入行程目的地的程式碼(例如,200表示總站)。

(2) 乘客可以通過車票鍵盤選擇車票種類(單程票、多次往返票和座席種類)。

(3) 繼續/取消鍵盤上的取消按鈕用於取消購票過程,繼續按鈕允許乘客連續購買多張票。

(4) 螢幕顯示所有的系統輸出和使用者提示資訊。

(5) 插卡口接受MCard(現金卡),硬幣口和紙幣槽接受現金。

(6) 印表機用於輸出車票。

(7) 所有部件均可實現自檢並恢復到初始狀態。

現採用物件導向方法開發該系統,使用UML進行建模,繪製該系統的初始類圖。

參考解決方案:

參考類圖如下:

類說明:

類 名
說 明
Component 抽象部件類,所有部件類的父類
Keyboard 抽象鍵盤類
ActionKeyboard 繼續/取消鍵盤類
TicketKindKeyboard 車票種類鍵盤類
DestinationKeyboard 目的地鍵盤類
Screen 螢幕類
CardDriver 卡驅動器類
CashSlot 現金(硬幣/紙幣)槽類
Printer 印表機類
TicketSoldSystem 售票系統類

方法說明:

方法名
說 明
Component 的init()方法 初始化部件
Component 的doSeltTest()方法 自檢
Keyboard的getSelectedKey()方法 獲取按鍵值
ActionKeyboard的getAction()方法 繼續/取消鍵盤事件處理
TicketKindKeyboard的getTicketKind()方法 車票種類鍵盤事件處理
DestinationKeyboard的getDestinationCode()方法 目的地鍵盤事件處理
Screen的showText()方法 顯示資訊
CardDriver的getCredit()方法 獲取金額
CardDriver的debitFare()方法 更新卡餘額
CardDriver的ejectMCard()方法 退卡
CashSlot的getCredit()方法 獲取金額
Printer的printTicket()方法 列印車票
Printer的ejectTicket()方法 出票
TicketSoldSystem的verifyCredit()方法 驗證金額
TicketSoldSystem的calculateFare()方法 計算費用