16/02/2006

物件導向不再無限上綱

這是我在之前的 blog 中所提到的觀點。有幸 Jini 也在他的 blog 中提出回應,讓我有機會,將這樣的想法作更進一步釐清。

首先讓我聲明一點,在我的前文中,以「EJB 3.0 叫好不叫座」作為標題的一部分是不適當的,因為該段的內容所評論的,主要是針對物件導向方法與軟體架構設計而發。把 EJB 3.0 放在一起討論,將不相關的議題混在一起,模糊了敘述的主軸。

所以,在這篇 blog 中,我將針對物件導向方法與軟體系統設計上的一些心得,加以陳述。

* 典範轉移,物件導向不再獨領風騷

近兩三年來的確有許多軟體設計上的新思維,取得了前所未有的注目,諸如 Aspect-Oriented Programming(AOP)、Service-Oriented Architecture(SOA)、Meta Programming(以 Java 來講是 Attribute/Annotation-Oriented Programming、Declarative Programming,甚至簡單講就是 reflection)。當然這些程式設計典範(paradigm) 在推出時,都會強調並不是用來取代物件導向設計,而是與物件導向相輔相成。

讓我這樣說吧! 以上所出現的這些新的設計典範,沒有一個是在設計小程式時需要用著的。你會發現不管是 AOP, SOA, MP 甚至是簡單的 reflection,都是在設計大系統、設計 framework 才會用著的東西。例如,EJB 3 中大量採用 Annotation 來簡化部署描述,為什麼?以物件導向的觀念來說,一個系統最好全部採用物件導向的機制來實作,這樣才有「概念的整體性」性嘛。Annotation 算是物件導向技術嗎?我不認為。那為什麼還要犯了「概念整體性」、「思想純淨性」的忌諱採用這種方式呢?

原因是很簡單的,當我們(至少是我)開發大系統或 framework 時,我需要元件(想像成 EJB) 與平台(想像成 JavaEE 容器) 間,或各個元件彼此之間有更容易的溝通與互動方式,也就是我前文所講的:

當你往上堆砌系統,或一開始即採用架構導向系統開發方法(Architecture Oriented System Development)時,比較適當的思維模式,是將系統看作一部"機器",你考慮的應當是系統各部元件之間的連結,彼此間如何互動,最後才是各部元件 各自的角色與職責。

以純淨的物件導向技術來實作的話,容器或框架會定義出一些(有時是很多)介面,讓符合其規格的元件實作。當然這就算是一種 IoC(Inversion of Control),一種 callback。而這也是 EJB 2 大量採用的方式。但為什麼 EJB 3 要採用 Annotation 呢?這證明什麼,至少在某方面,Annotation 的思維可能較之物件導向思維,更適於解決某些系統間的溝通、互動角色。

* SOA 與物件導向思維之關係

SOA 的確如 Jini 所說,是透過支援一致的呼叫介面,讓異質系統可以互相串連的(當然用在同質系統中也行啦!)。然而我認為服務導向(或訊息導向) 與物件導向精神還有一個很大的不同點:服務導向架構更傾向使用 coarse-grain 的 API。相對的,物件導向方法為了充分表達領域物件的語意,通常會設計出 fine-grain 的 API。coarse-grain API 跟 fine-grain API 有什麼差別呢?

我以 EJB 中最符合 SOA coarse-grain API 型式的 JMS 來說明。我們撰寫 MessageDrivenBean 時,通常只需撰寫 MessageListener 的 onMessage(Message inMessage) 方法,然後在這個方法中,呼叫其他的物件(有可能是以 fine-grain style 寫成),就可以製造出一個能夠自由組裝資訊流程的系統。試想,如果我們這裡不用 coarse-grain API,而用 fine-grain API 來作為訊息傳遞介面會如何?我的經驗是,fine-grain API 大大的阻絕了物件/類別的重用性。這一點只要想想,用 SessionBean 是否可以像 MessageDrivenBean 那樣擁有組裝的靈活性就可明白。所以這裡的重點是,fine-grain API 適合用來表達領域物件,而 coarse-grain API 適合用來作為整個系統的通訊介面。

* Annotation、Reflection 與物件導向觀念可能潛藏的衝突

這裡我說的是 "潛藏",也就是在不良設計的情況下,並不是每個用到 Annotation、Reflection 技術的程式都會與物件導向觀念產生衝突。我從物件導向的三個特性說起。物件導向的特性是「封裝」、「繼承」與「多型」。接著我們再來想想 Annotation 的運作方式,它是在原始程式碼中定義一些 -- 好吧,就是 annotations,然後呢,在 compile time 或 rum time 時針對這些 annotations 做些處理。

當我們自己寫程式去處理 annotation 時,事實上是敲開一個類別,東看看、西看看 -- 偷窺別人的內在總是自在又痛快的事情,如果能夠痛下 鹹豬手自由 "存取" 一番,那更是淋漓盡致啦 -- annotation + reflection 給你這項偷偷摸摸的特權。而 (該死的) AOP, byte code generator 更讓你能打開類別的外衣,在裡面亂搞一通。

最明顯的,這裡嚴重的侵犯了類別的隱私(封裝性)。透過 reflection,你可以把類別的祖宗八代都叫出來。然後呢,有些本來該是用多型手法解決的問題,卻透過 annotation 或 AOP 之類的技法去解決。就算你的 annotation 或 AOP 設計精良,很可能程式的可追蹤性已變差;若是設計不良,就會產生比用 switch 敘述來取代多型的 bad smell 更難聞的 terrible smell. 所以這裡重點是,Annotation 不但與物件導向無關,甚且有害於物件導向。為採用 Annotation 的系統框架所寫的元件,極可能比採用 Dependence Injection 進入系統中的 POJO 元件更不易在別處重用。

* 同樣是 AOP,也有符不符合物件導向精神的區別

AOP 有好幾種實作方式,一種是直接在 java 語言中,增加支援 AOP 的語法,然後在編譯期將 AOP 的語法 waving 到 class byte code 中,這是 AspectJ 採用的方式。另一種是在架構上,以物件導向的方式(例如 proxy/interceptor pattern),實作對 AOP 的支援,這是 Spring AOP 所用的方式。通常前一種方式對 AOP 特性的支援較為完整,例如可以在 statement 中對被剖析的程式動手腳(加入 advice);後者則較差,像 Spring AOP 最細的 pointcut 就只到 method 層級。

照理說既然第一種方式對 AOP 的支援較為徹底,那我們應該多多採用 AspectJ 囉?事實上,我個人卻較傾向 Spring AOP。原因是,在 Spring AOP 中,可以用物件導向方式解決的,都盡量採用物件導向方式解決了。在 Spring AOP 中,不管是 pointcut 或是 advice 都是在實作特定的介面,因此這些類別,就算離開 Spring AOP 的環境,也還有重用的機會(只要你高興,你可以在一般的物件中呼叫)。而 AspectJ 的作法是增加 java 的語法,加入 aspect 元素(就像 class 元素一樣),在其中定義 pointcut。然而 aspect 與 class 的關係是單向的,不用說這些 aspect 不能離開 AspectJ 的編譯器下運作,就算是一般的類別,都不能呼叫這些 advice(對,懂得人會問為什麼要這麼做?我的理由是,給我自由。)

這裡的重點是,對於解決一個問題,你可能有好幾種作法。這幾種作法中,有些作法可與現有系統完美的搭配,而有些作法卻是充滿阻抗,與系統格格不入。選擇很明顯,給我 AOP,但請盡量用物件導向的方式。

* 有時候就是用不到物件導向的思維

或說的仔細一點,不要物件導向過了頭。例如,你在開發報表程式,你希望這樣的程式可以在資料庫欄位異動時,仍可在畫面上正常顯示。這時候,你絕不會想要對 報表所在的表格進行領域塑模。因為這時候你該注意的,不是表格代表的領域觀點,反而是所有資料庫中共有的架構特性,你該塑模的,是想辦法將資料庫綱要,與報表的樣版合協互動的方法。沒錯,報表程式中的資料庫綱要在 Java 中的確也是用 class 來表示,你會比較看重它的連結(介面)觀點。

其他的程式,像是 OLAP 程式,或是可以自由組裝欄位內容、資訊分類的 CMS 系統,我也是寧願採用較為簡單的 ER 觀點。直接處理表格裡面的欄位資料,會比較具有靈活性。這有點類似於 coarse-grain API 的作法。

* Java 失去物件導向的純淨性,卻換來開發應用系統更大的自由

Java 1.0 與現今的 Tiger 相較,哪個 Java 版本較接近純淨的物件導向語言呢?無疑的大家會同意是 Java 1.0。任何事物一開始出現,總是有美好的中心思想,但受到世俗濁流的影響,不可避免的要由純淨走向混亂。有些語言為了避免被人發現這個現象,乾脆直接招認。像是 C++ 說自己是 Multiparadigm Programming,Perl 的哲學是 There’s More Than One Way to Do It。那麼 Java 呢?

當物件導向取代結構化程序設計觀點時,我們也是花了好久習慣,但終究沒有引起太大的阻力。原因是物件導向的功能完完全全的含括了結構化設計的所有功能。今天的困難是,還沒有一種獨立、總括的思維,可以完全含括物件導向的思維。或許架構 + 元件算是吧! 但是為了解決架構與元件間連結、互動的問題,我們採用了種種沒有中心思想的作法。失去了純淨,增加了選擇。

結論

群雄並起,各領風騷。物件導向架構的語法元素,取代程序性語法元素,成為基本的建構單元。今天我們看待一個 class,就像往昔我們看待一個 procedure, function 那樣自然。

一開始的時候,程式是資料結構 + 演算法。後來呢,程式變成是一堆一堆物件的連結,但其實物件裡面也是資料結構 + 演算法。而今呢,注意一下你的企業系統:Data Warehouse + SOA。Data Warehouse 是比較大的資料結構,SOA 是比較大的演算法(流程 -- 活動的組合)。

看起來好像沒什麼不同,不過,以前你企業裡面的資訊系統是一座座孤島。而今呢?整個企業是一支可以合協運作的大程式。這就像是一堆單細胞生物,與一個人的區別。

06/05/2005

Open for Extension, Closed for Modification

除了 Design Patterns 之外,近年來物件導向設計領域還開始講究 OO principle. 像是從前年開始流行起來的 micro kernel framework -- PicoContainerSpring Framework 所講究的 dependency inversion,本身也是一種 principle。而 RMI, EJB 及 Jini 等遠端物件技術,則符合另一 principle -- Program to an interface, not an implementation. 這些 principle 之所以存在,最主要的目的就是讓我們在設計系統時,可以增加系統的擴充性與維護性。對這些 principle 有與趣者,可以到 Object Mentor 尋找相關資料。

由於昨天部門讀書會剛好討論到這個議題,我便就這個主題發表一下我的看法。像是這個讓人覺得矛盾的 Open Closed Principle, 也就是 Open for Extension, Closed for Modification,到底 Open (開放)了什麼東西,Closed (禁制)了什麼東西。用簡單的話來講,這項原則強調:

* 在設計一類別時,應保留足夠的彈性,讓採用此類別的程式,透過擴充的手法,即可新增或變更系統功能。
* 在設計一類別時,應具有適當的封閉性,避免讓採用者直接修改類別原始程式,才可達成其功能。

我想寫過稍大一點程式的朋友一定會這種經驗,當我們在程式中採用一個類別時,有時候發現該類別少了許多我們所要的特性或功能,這時候我們可能採取以下幾種作法,來達到呼叫者所要的功能:

* 你可能動手直接修改該類別某個 method 的實作
* 你可能在該類別中新增你所須要的 method
* 你可能透過建立一個該類別的子類別,再採用其子類別,來達到你所需要的功能
* 你可能將該類別的某些職責(responsibilities),透過 composition 或 delegation,交給別的物件處理,就可以達到你所需要的功能

究竟採取何種作法,除了視採用者個人的偏好外,還與原始類別設計的好壞有關。各位可以看到,上面的這幾種方法中,越上面的方法,對你原本系統的程式修改越多,而越下面的方法,對原程式的修改越少。因此如果一個類別的設計,能讓採用者越喜好使用下面的方法來達成其功能,則原始類別的設計,就越符合 Open Closed Principle。

我再舉一個 JDK 的例子,在我們使用 JDK 裡面的 API 時,就不太可能會想要動手去修改 API 裡面的實作。例如:當你發現 JButton 少了你所需要的屬性時,你大概不會動手修改 JButton 的原始碼;反之,你可能會新建一個 JButton 的子類別,再於其中加入自己所需的屬性,這就符合了Open Closed Principle (甚至所有的 JComponent 都允許你用 putProperty 的方式,在不用新建子類別的方式之下,動態新增屬性)。還有不少 JDK API 的設計,也都符合這項原則,像是 sort 只要傳入 Comparable,JButton 有 addActionLisenter, JTable 有 TableModel... 不勝枚舉。

22/03/2005

你所想像不到的 JavaScript

這不是一篇教你如何在網頁中應用 JavaScript 的教材。這是一篇讓你明瞭看似簡單的 JavaScript,其實其核心語法功能強大。我將在這篇文章中說明 JavaScript 物件導向的特性,並說明如何透過這些特性,達到傳統程式語言 (C++/Java) 所難以完成的功能。

Written by Edward Hsieh/謝鎮澤

January 04, 2002

Everything is Object in JavaScript

開宗明義:在 JavaScript 中任何東西都是物件:變數是物件、函式是物件,常數也是物件。證明方式:

 alert (typeof('abc')); alert (typeof(123)); var ary = [123, "abc"]; alert (typeof(ary)); 
上面的 typeof 會顯示物件資料型態。得到結果分別是 string, number, object。

Create Objects in JavaScript

既然任何東西都是物件,那建立物件的方法就多了。直接看個例子:
 var main = new Object;     // 建立新物件 main.x = 123; // 設定物件成員變數(屬性)之一 main["y"] = "XYZ"; // 設定物件成員變數(屬性)之二 alert(main["x"]); // 取得物件屬性並輸出 alert(main.y);     

可以看到在 JavaScript 中,main.x 與 main["x"] 這兩種語法是通用的。其實在其他語言中,這兩種表示法的語意並不相同。我稍後再作說明。

List All Members in an Object

這是 JavaScript 的必殺技,使用 JavaScript 的人務必要學會這個技巧。底下函式可以傳回一個物件的所有成員的字串表達式,包括物件中的屬性及方法。在物件導向程式設計中,這種技術叫 reflection。

 functin listMember(main) { var s = ""; for( key in main ) // 使用 in 運算子列舉所有成員 s += key + ": " + main[key] + "n"; return s; } 
範例碼中的 key 會對應到物件中的屬性名稱,如 "x" 或 "y",而 main[key] 則對應到屬性值。

說這項技巧是必殺技的原因是,你可以透過這項技巧,將物件封裝的黑箱打開來,看看裡面藏有什麼東西。我常用這項技巧來看看 IE 與 Mozilla 的 Dom 物件模型有何不同。試試看下面呼叫範例,就可以知道這項技巧的強大了:

 var ary = [123, "abc"]; alert (listMember(ary)); alert (listMember(document.location));  

Construct Object with Initial Value

要在建立物件的同時指定物件初始值,必須先透過 function 建立一個「原型物件」(或稱為 constructor),再透過 new 運算子建立新物件。例如以下程式碼會建立一個二維陣列的原型,再產生一個新的二維物件。
 function Array2DVar(x,y) {     // 定義二維陣列原型 this.length = x; this.x = x;        // x 維度長度 this.y = y;        // y 維度長度 for(var i = 0; i < this.length; i++)  // 初始各元素值為 null this[i] = new Array(y);    // this 代表物件本身 }  var a2dv = new Array2DVar(10, 10);   // 建立新的 10*10 的二維陣列 a2dv[1][3] = "ABC";       // 設定二維陣列元素值 a2dv[2][6] = "XYZ"; a2dv[9][9] = 1000;  alert( a2dv[1][3]);  // 取得二維陣列元素值,並顯示出來 alert( a2dv[2][6]); alert( a2dv[9][9]);  
Initial Array Object 在 JavaScript 中陣列也是物件 (其實近代多數語言中陣列也都是物件,只有像 C 或 Assembly 這類古老的語言才不把陣列看成物件),因此也可以用 constructor 的語法來建構。當然 JavaScript 還提供了 [] 語法,以更方便建構陣列,範例如下:
 a = new Array("abc", "xyz", 1000); // constructor 語法,或 a = ["abc", "xyz", 1000]; // 陣列標準語法  
陣列的元素可以是簡單的資料、其他物件,或是函數。舉個例子來在陣列裡面放函式:
 b = [ // 使用函式作為陣列元素 function () { alert("這個好玩!") }, function () { alert("再按一次離開!") }, function () { alert("再來一次!") }, function () { alert("最後一次!") } ];  for (var i = 0; i < b.length ; i++) b[i]();  

最後一個 for 迴圈是個有趣的應用。由於 b 陣列中現在存放的所有元素都是函式,因此我們可以對 b 的每個元素進行呼叫。

Object as Association Array

關連陣列 (Assocation Array) 又稱作 Map 或 Dictionary,是一種物件容器,其中可以放置許多的 key-value pair,以存取子物件。在JavaScript 中,物件本身就可以作為關連陣列。以關連陣列的方式初始化物件的範例如下:

 obj1 = {"a" : "Athens" , "b" : "Belgrade", "c" : "Cairo"}; alert(obj["a"]); // 顯示 Athens obj2 = { name: "Edward", showName: function() { alert(this.name); }  // 使用函式作為物件屬性 } obj2.showName();  // 顯示 Edward obj2.age = 23; // 屬性可以動態加入 
其 中 obj1 儲存了三個子元素,其鍵 (key) 為 "a", "b" 與 "c",而值 (value) 為 "Athens", "Belgrade" 與 "Cairo"。obj2 中 showName 鍵所對應的值為 function,因此 obj2.showName() 即為函式呼叫。

Object as Return Value

雖然 Javascript 的函式只能傳回一個變數,但您卻可以將傳回值設定為物件,達到傳回 1個以上變數值的效果
 function a () { return [32, 17]; } b = a(); alert( b ); // 或 alert(a()); 
function pixel () { return {"x": 32, "y":17}; }
point = pixel (); alert (point.x + "n" + point.y); // 或 alert (pixel().x + "n" + pixel().y);

Delegation Function Object

函式也是物件,只是其中包含的是程式的邏輯。這項特性可拿來作為委任式的程式設計,亦即使用委任函式當作另一函式的參數:
 function doloop(begin, end, func) { // 這個函式是個 iterator for (var i = begin; i < end; i++) {  func(i); } } function func1(i) { // 印出 ** n ** document.writeln("** " + i + " **
"); } doloop(1, 10, func1); // 印出 1o 行 ** n ** doloop(20, 29, function(i) { // 印出 1o 行 ## n ## document.writeln("## " + i + " ##
"); });
Object = Properties + Behaviors

古有明訓:程式 = 資料結構 + 演算法。而物件是建構程式的基本單位,自然的具有相同的性質。物件除了有屬性 (property),也可具有操作 (behavior),也就是函式。

假如我們要使用一維陣列來模擬二維陣列,那麼就無法使用 ary[x][y] 這種表示法來設定或取得陣列成員。不過我可以定義一個 set 方法來設定成員變數,而以 get方法來取得成員變數值。原型函式定義如下:
 function Array2D(x,y){      // 以一維陣列模擬二維陣列的原型物件 this.length = x * y;      // 陣列總長 this.x = x;         // x 維度長度 this.y = y;         // y 維度長度 for(var i = 0; i < this.length; i++)  // 初始各元素值為 null this[i] = null; this.get = function(x,y){      // 成員函式:取得陣列第 [x,y]個元素值    return this[x*this.x + y]; }  this.set = function(x,y,value){  // 成員函式:設定陣列第 [x,y] 個元素值 this[x*this.x + y] = value; } } 
我們接著來使用它:
 var a2d = new Array2D(10, 10);    // 建立新的「二維」陣列  a2d.set(1, 3, "ABC");   // 設定「二維」陣列元素值 a2d.set(2, 6, "XYZ"); a2d.set(9, 9, 1000);  alert( a2d.get(1,3) );  // 取得「二維」陣列元素值,並顯示出來 alert( a2d.get(2,6) ); alert( a2d.get(9,9) );    

Member Function Outside of Constructor

我們也可以將物件成員函式寫於原型物件之外。以下的Array2D物件與上一個範例中的 Array2物件有相同的作用,只不過這次是寫在原型物件之外。
 function Array2D(x,y){      // 以一維陣列模擬二維陣列的原型物件 this.length = x * y;      // 陣列總長 this.x = x;         // x 維度長度 this.y = y;         // y 維度長度 for(var i = 0; i < this.length; i++)  // 初始各元素值為 null this[i] = null; this.get = Array2DGet; // 用這種方式把成員函式掛進來 this.set = Array2DSet; }  function Array2DGet(x,y){      // 成員函式:取得陣列第 [x,y] 個元素值 return this[x*this.x + y]; }  function Array2DSet(x,y,value){    // 成員函式:設定陣列第 [x,y] 個元素值 this[x*this.x + y] = value; } 
Dynamic Object Function

這裡說明如何為一個已定義物件,動態的加上其他操作的方法。

如 果一物件已定義完成,而您也使用它來建立了新的物件,這時候您想為原型物件增加新的操作 (而不修改原型物件的原始碼),讓所有該物件的複本都能使用該操作,該如何達成呢?方法是使用物件的 prototype 屬性。以下這個例子,為 Array 這類 Object 在執行期加入一個 max 方法,以取得陣列元素之最大值 (修改自微軟 jscript.chm之範例):
 function array_max(){      // 定義求取 Array 最大值之函式 var i, max = this[0]; for (i = 1; i < this.length; i++){ if (max < this[i])   max = this[i]; } return max; } Array.prototype.max = array_max;   // 在 Array 原型中加入 max 函式 
上面的程式碼,首先建立一個 array_max 方法,以求取陣列之最大元素。接著將這個方法設定給 Array 原型物件。
 var x = new Array(1, 2, 3, 4, 5, 6);  // 透過 Array 建構子建立一陣列                                   // 想求取 x 中某一元素之最大值 var y = x.max( );       // 取得 x 之最大元素 
Dynamic Mix in

假 如物件 dynamic 有 mathod1, method2 兩個函式;而另一物件 main 有 methodA 及 methodB 兩個函式。現在我想把 dynamic 的所有特性 (feature) 匯入到 main 中,我們可以在 main 中加上一個 imports 函式:

 function main(){              // main 之建構子 // ... this.imports = function (object) {   if( typeof object == "object")     for( value in object )       this[value] = object[value]; } // ... } obj = new main(); main.imports(new dynamic());   // 匯入 dynamic 物件之所有功能 
這個 imports 函式可以動態的為 main 加上另一物件的所有操作。這種 Mix in 的功能可是 C++/Java 的 static type 語言所望塵莫及的。

09/07/2004

Software: Object Oriented...

昨日心血來潮,把放在書桌一角,塵封已久的一本書
Object Oriented Analysis (2nd Edition)
by Peter Coad, Edward Yourdon
看了一遍。內容包含:
 Introduction. 1. Improving Analysis with Object-Oriented Techniques. 2. Experiencing an Object Perspective. 3. Identifying Objects. 4. Identifying Structures. 5. Identifying Subjects. 6. Defining Attributes. 7. Defining Services. 8. Moving to Object-Oriented Design.   
在網路上找了一下,哇! 這裡 有不錯的資源...

See also: