類別之間的關係(Relationship) — 整體-局部(Whole-parts) (2)

說明

“整體—局部(Whole-parts)” 可以說是降低物件複雜度(Complexility)的最有效機制,因為,你可以將某一物件視為是一個 “整體(Whole)”,只要聚焦從該物件外面的角度來觀察該物件的特徵與行為,不用去關心組成該物件的細節,包括其組成的內部元素。其實,不探究物件內部的組成元素,就是一種封裝(Encapsulation)的效果,而封裝正是軟體設計人員在處理軟體的複雜度時,所必備的本質素養。

從外部的觀點來看待物件,焦點會集中在整個物件的特徵與行為,這是屬於 “用” 的角度,也就是只要知道如何能 “使用” 該物件所提供的服務(service)與取用該物件的資訊即可;那個時候需要 “剖開” 該物件的內部,來分析其內部的組成元素(也就是組成物件)? 這是屬於結構分析/設計的工作。

例如,以上圖「駕駛員」與「車子」這兩個類別來說,”駕駛員” 並不需要知道 “車子” 的內部結構,只要知道 “如何開” 這輛車子,也就是說,”駕駛員” 只要知道如何使用 “車子” ,包括開動、踩油門、轉動方向盤、排檔、煞車等,以及看得懂車子儀表板所提供的資訊就可以了。如果車子壞了怎麼辦?此時 “駕駛員” 可能必須 “打開” 車子的前車蓋,檢查內部是哪一些組件出了問題,更甚者,必須送至車廠檢修,利用檢修的電腦儀器來找出更細部的組件故障問題,這是屬於結構分析範疇的一種形式。而從汽車製造廠的角度來看車子時,更是需要對車子內部的結構詳加分析,並進一步的要能設計具體化為實體的產品,當然就必須對其組成車子各部分的結構元素要能瞭解彼此之間的關係,與各個結構元素的特性等。

從以上的敘述中可以得知,駕駛員不需要知道車子內部的結構,所以可以把車子視為是 “整體” 的單一個物件。但從汽車製造廠設計人員的角度來看車子時,卻是將車子視為是 “複合物件”,所以必須分析其內部的組成元素,這就是屬於結構分析與設計的範疇。

在生活面中,充滿了太多的 “複合物件” 的實例,例如,MP3 Player,筆記型電腦,主機板,樹木、乃至於中央山脈(包含了山峰、丘陵等局部) …等;企業面(Enterprise)中,公司(包含了多個部門)、訂購(包含了多個訂購細項)、看診(包含了多個診療記錄) …。

複合物件內部的結構元素是否也有可能是複合物件? 當然會的,例如,站在電腦主機板設計者的角度來看待 INTEL CPU 時,CPU 即為複合物件,但,卻是沒有必要分析其 CPU 內部的結構,只要知道如何與之溝通即可,而溝通的管道,就是透過 “介面(Interface)” 的呼叫;相對而言,INTEL 設計者就需要知道組成 CPU 內部的結構元素,例如包括邏輯運算處理器、暫存器、控制單元 …等。而事實上,CPU 其實看起來也像是 “主機板” 的角色,因為它也是需要 “組合” 多個內部的結構元素,但同樣地,它也沒有必要去瞭解如運算處理器內部的結構。

組成關係可以被視為是一種特殊的結合(a special kind of association)關係,所表達的是在於整體與其組成元素之間的一種結構(structure)關係。組合關係有兩種的表達方式,聚合(aggregation)在 UML 中是以空心菱形符號來表示;組合(composition)關係在 UML 中則是以黑色菱形符號來表示。兩者的 UML 表示法如下圖。

範例—聚合關係的UML表示法
圖1、聚合關係的UML表示法

範例—組合關係的UML表示法
圖2、範例—組合關係的UML表示法

聚合與組合關係的主要區別在於整體物件與局部物件的生命週期問題,聚合關係是屬於比較寬鬆的組成關係,以上圖 1為例,主機板包含了CPU、顯示卡、記憶體模組等組件,而這些組件均可以隨時視需要來 “抽換(Plug and Play, PnP)”,而圖 2 的例子則表達了比聚合更為嚴謹的結合形式,當 “Web Form” 實體(instance) 被系統給消滅(destroy)的同時,其所包含的組件,包括 “Button”、”Label”、”TextArea” 等,均會隨之與其整體(Web Form)被消滅,也就是說,整體與局部的組件,係為一整個生命共同體,當整體消滅時,其局部的組件也無法存續。

範例—駕駛員與汽車的組成關係

UML 表示法

範例—車子內部結構的組成關係
圖3、範例—車子內部結構的組成關係

“車子” 是一個 “整體(whole)”,其組成的結構元素包括了 “引擎” 與 “輪胎”,其多重性(multiplicity)個別是 1對1(一台車子有一個引擎)、1對4(一台車子有四個輪胎)。其中,車子與引擎的生命週期是等長,引擎無法被抽換,所以是以 “黑色菱形” 符號的組合關係來表示;車子與輪胎則是以 “空心菱形” 符號的聚合關係來表示。

注意的是,外界無法直接來操作與存取 “引擎” 所提供的操作,必須要透過 “車子”來取得引擎的相關資訊,所以 “駕駛” 是透過 “車子” 所提供的 “get引擎狀況” 操作,再由 “車子” 去詢問 “引擎” 的 ““get引擎狀況” 操作,來取得相關的資訊的。

局部(part)的零件,只能透過由整體(whole)來存取操作,而不可讓外界來直接存取使用,這是 “封裝(encapsulation)” 的基本設計原則。

※延伸參考:「觀察電動牙刷的結構設計

Java 程式範例碼

@原始碼下載

#001 public class 車子 {
#002 
#003 	public 引擎 m_引擎;
#004 	public 輪胎[] m_輪胎;
#005 
#006 	public 車子(){
#007 		m_引擎 = new 引擎();
#008 		m_輪胎 = new 輪胎[4];
#009 	} 
#010 	
#011 	public String 踩油門(){
#012 		String 滾動="";
#013 		
#014 		//需要實作四個輪胎的同時滾動,請賴小仁以 Thread 機制實作
#015 		for (int i=0 ; i<m_輪胎.length ; i++){
#016 m_輪胎[i] = new 輪胎();
#017 滾動 = 滾動 + "輪胎:" + i + " " + m_輪胎[i].滾動() + "; ";
#018 }
#019 return 滾動;
#020 }
#021
#022 public String get引擎狀況(){
#023 return m_引擎.get引擎狀況();
#024 }
#025 }
#001 public class 輪胎 {
#002 
#003 	public String 滾動(){
#004 		return "滾動中...";
#005 	}
#006 }
#001 public class 引擎 {
#002 
#003 	public String get引擎狀況(){
#004 		return "引擎狀況良好...";
#005 	}
#006 }

一樣,我們仍設計 “TestCar”,從駕駛的角度來看車子的運作情形。

#001 public class TestCar {
#002 	public static void main(String[] args) {
#003 		車子 m_car = new 車子();
#004 		
#005 		System.out.println(m_car.踩油門());
#006 		System.out.println(m_car.get引擎狀況());
#007 	}
#008 }

執行結果如下:

輪胎:0 滾動中...; 輪胎:1 滾動中...; 輪胎:2 滾動中...; 輪胎:3 滾動中...; 
引擎狀況良好...