類別之間的關係(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 滾動中...; 
引擎狀況良好...

文章導覽

   

共有 10 則迴響

  1. 可否請教一下,關於”part只能透過whole來存取操作,而不可讓外界來直接存取使用”這個概念。

    如果以駕駛與汽車的範例而言,駕駛需透過汽車了解引擎狀況,不能直接接觸引擎。
    但假若今天駕駛本身就是汽修員,換句話說,我需要同時考慮駕車與修車的兩種狀況,
    那我應該以誰為主體呢? 還是就退化回一般關聯關係,不再牽扯whole-part?

    • 維修員是需要探究「汽車」內部的結構元素,對他的角色而言,就不是把「汽車」當黑箱、而是需要剖開成為「白箱」的設計了。

      Whole-part 關係仍是存在的,只是端賴各類不同角色的 Client 而決定是僅從 Whole 整理看待整體的 “用”,或是可以探究內部 part 的組件結構。

  2. Hi Victor:

    我仍是強烈建議,在討論這類設計議題的時候,能先以視覺化的方式,來表達物件的互動,這樣會清楚很多。你們應該先能畫出圖來,再來討論,會比較容易聚焦。

    至於物件是誰呼叫誰、誰創造誰等,這是牽涉到生命週期,可視性等相依性的分析。

    至於是否用 prototype 之類的設計模式,那實在與 OO 設計思考沒什麼關係的。

  3. Hi Victor:
    我會建議你可以先利用物件溝通圖如循序圖來表達出兩個物件的訊息互動情形,以及觀察兩個物件的生命週期(life-cycle)。如此你可能會比較知道在程式寫作的表達上。

  4. 請問可以請教各位一個關於OO的問題嗎???我用的prototypejs

    這裡有兩個class, FormExt, AjaxRequest
    我希望FormExt.submit()會新增一個AjaxRequest的Object
    而當AjaxRequest.onComplete時,則會自動call FormExt() 的reset Method

    是否必須把FormExt的object以參數形式傳入AjaxRequest嗎?
    現在的情況是A object下產生了B object,然後把A object pass進B object,感覺怪怪的

    程式大概是這樣
    var FormExt = Class.create({
    initialize: function(id){
    this.id=id;
    this.obj=$(this.id);
    this.ajaxObj=”;

    this.eventSubmit=this.submit.bindAsEventListener(this);
    Event.observe(this.id, ‘submit’, this.eventSubmit);
    },

    submit:function(e){
    this.ajaxObj=new AjaxForm(this.id);
    this.ajaxObj.startAjax();
    break;
    },

    reset:function(){
    this.obj.reset();
    }
    });

    var AjaxRequest = Class.create({
    initialize: function(id){
    this.frmObj=$(id);
    this.ajaxObj=”;
    },

    startAjax: function(){
    this.ajaxObj = new Ajax.Request(
    ‘abc.php’,{
    onComplete:this.onComplete.bind(this)
    }
    );
    }

    onComplete:function(){
    alert(‘complete now’);
    }
    });

  5. Hello 沈鸔:
    我反而覺得物件導向最好不要去談繼承,因為,我實在不認為有所謂的「繼承」。^^
    要說最重要的哲理,那麼,封裝(encapsulation)可以說是我個人最為重要、最為基礎的功夫。

  6. Kenming Wang,
    你好,謝謝您!
    你的回答,算是應證了我的看法,可能也因為這樣,在教與學OO的特徵時,容易被誤導。
    我認為,以程式語言而言,物件語言唯一應該教或學的只有繼承,其他都是繼承應用的技巧(或者說是語法的邏輯與規範),所謂多型、聚合、介面等應該只是系統分析與設計領域的問題。
    如果不作區隔,學生往往只有死背,而且觀念也不通。

  7. 您的例子非常清楚,不過有個細節想請教。
    在這個例子中,composition與aggregation在java 實作上,如何區別呢?

發佈回覆給「沈鸔」的留言 取消回覆

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *