物件導向的基本觀念
前言
物件導向系統是由物件所構成,但是大多數的人似乎從來沒有仔細思考過一些問題,例如:
物件導向到底是什麼?背後的原理為何?…
物件的概念是物件導向最基礎的概念之一,但是許多人對物件的認知似乎只不過就是「封裝」、「繼承」、和「多型」這樣而已。如果基本功夫不先練好,未來許多更高深的武功學起來也將事倍功半了。
在臺灣,許多人進入物件導向的領域多是由OOP入門。但是OOP通常假設讀者已具備足夠的物件導向觀念。同時OOP只是程式語言實作物件導向觀念的產物,在許多物件導向觀念上並未清楚地呈現與區分,所以若是在物件導向觀念尚未建立好的情況下學習OOP,結果反而會造成許多觀念上誤解。本文的目的就是在澄清這類的誤解。
這篇文章中以GoF的「Design Patterns註」為基礎來整理出一些物件導向的基本觀念,希望能讓大家對物件有更清楚與深刻的認識,在面對現今迅速發展的物件導向相關技術與方法論的同時,能夠更快速掌握其要點與精髓,而不至迷失在複雜的技術細節中。
這篇文章的主要目標對象是已經稍具OOP觀念的讀者。如果你沒學過OOP,或者OOP尚無足夠的經驗,這篇文章也可以提供一些基本的物件導向觀念。
在閱讀這篇文章時請不要讀得太快,因為這是篇闡述觀念而非應用技巧的文章。許多概念都依賴前面所定義的概念,所以在往下讀之前請先確認對已讀過的概念已有清晰的理解。
註:Eric Gamma, Richard
Helm, Ralph Johnson, and John Vlissides. Design Patterns – Elements of
Reusable Object-Oriented Software. (有中譯本,培生出版,葉秉哲
譯)
軟體開發流程的哲學
不論是RUP,XP,與軟體生命週期等的軟體開發流程,其實都是用一種「由高階觀點逐步往下對應至低階實作的過程」的哲學來開發軟體系統。基本上各家學說都還是秉持著分析、設計、實作、與測試的步調,只是對在此開發過程中的一些議題有著不同的觀點。這是因為大家的目的都是為了「以最低的成本,最高的效率,在時程之內,將具有合理品質的軟體交付給客戶」,所以該遇到的問題其實都是一樣的。各家學說也基於這樣的目的,所以嚐試為開發過程中可能會遇到的問題提出一套解答(當然也包含了它們的假設前題),並將這些解答加以系統化與標準化成為一套流程,讓大家可以照著做。
根據Martin Fowler(UML Distilled的作者)的說法,我們可以用概念觀點(conceptual perspective)、規格觀點(specification perspective)、以及實作觀點(implementation perspective)來看一個軟體系統。所謂實作觀點就是傳統上以程式碼觀點來看系統—對應到開發過程的實作階段,相信大家都已非常熟悉,所以不在此贅述。而規格觀點指的就是物件或子系統的介面規格—對應到開發過程的設計階段,至於概念觀點就是以物件的邏輯責任(responsibility)來看系統(do what)—對應到開發過程的分析階段。所以軟體開發可以看作是一個由概念觀點(高階),經過規格觀點,進而來到實作觀點的過程(低階)。
同樣地,物件也具備這三個觀點。由概念觀點可以定義出物件的責任,由規格觀點可以定義出物件的介面,而實作觀點即是物件的程式碼。而物件導向最重視的觀點就是概念觀點。
掌握了物件的概念觀點才有可能體悟到物件導向的精義,這對於OOA與OOD是非常重要的。許多人無法享受到物件導向的好處的原因常源於此,過度地以實作觀點來看物件導向系統是很難看到其精妙之處的,所以自然無法享用其利益了。
事實上本文中所解釋的觀念都蘊藏在許多設計模式(design pattern)之中,許多人讀了很久的設計模式後,經常還是難以體會其中的精髓,無法活用設計模式,充其量只能拿著GoF那本書的23個模式依樣畫葫蘆而已(甚至永遠只會其中那幾個模式)。相信若是完全理解本文中的觀念後,對設計模式會有更深的一分體認。
什麼是物件導向
物件導向方法的目的與優勢在於提高軟體重複使用的程度,讓軟體更易於理解與維護或修改,更重要的是能良好地因應需求的變動。這對現代日趨龐大複雜的軟體是非常重要的因素。這也是物件導向越來越風行的原因之一。
雖然物件導向有這樣的好處,但是真正能享受到好處的人似乎不多。其中的原因多出於對物件導向觀念的不良或誤解。事實上,相對於程序導向來說,物件導向更為複雜與困難。雖然物件導向好像一把威力極大的寶劍,但若是內力不足的話,即使得到這樣的利器,終究還是難以享受到美好的成果,不僅無法善用其優勢,甚至反而可能先誤傷了自己。
所謂登高必自卑,底下開始介紹一些物件導向的基本功夫。基本功練好了才有可能學好更高深的武功,不是嗎?J
Ø
介面
大家都知道,物件的client端(物件的使用端)會呼叫物件所定義的方法(method註)要求物件執行某些動作。唯一可以使用物件的方式便是透過物件的介面呼叫。在繼續說明物件介面的觀念之前,我們必須先複習一個基本的程式設計觀念:
函式(方法)簽名(signature)。
註:「呼叫」在物件導向理論中大都是用「物件的client端傳送訊息(message)給物件」這樣的說法。
一個函式(方法)的名稱、參數型態與個數、與回傳值構成了函式(方法)的簽名。要留意的是:函式(方法)簽名並不包括函式(方法)的實作部分(也就是程式碼的部分)。用C++的說法就是:函式的宣告(declaration)就是函式簽名,而函式的定義(definition)就是實作。在C++中,函式的宣告通常放在標頭檔(header file),而函式的定義(實作程式碼)通常放在CPP檔。
以下我們可以開始介紹一些物件介面的重要概念了:
n
介面(Interface)
物件介面指的就是物件所訂定的所有方法簽名(表示物件提供給外界使用的溝通方式,也就是可以送給此物件的訊息類型)。至於這些方法是如何實作出來的則不包含在介面的概念之中。
介面是物件導向最基礎的概念。我們想叫物件做任何事,都必須透過介面完成。
n
型態(Type)
型態是指某一個特定的介面(亦即特定的一組方法簽名)。例如當我們說:「A物件擁有Window型態」時,就是說A物件具有Window型態中所定義的一組方法簽名,A物件具有Window的行為,可以接受Window型態所定義的訊息。通常型態都具有一些邏輯上的意義,例如Window型態、Drawable型態、…等,這視我們的需求而定。
由型態的定義可知,物件可能同時具有多個型態,而多個物件也可能具有相同的型態。但是請注意:具有相同型態的物件並不一定表示有相同的實作。(記得介面與型態是不包括實作的嗎?)
n
繼承(Inheritance)
介面也有繼承的觀念。在介面繼承中,子介面會包含父介面。說得白話一點,就是子介面會包含所有父介面中訂定的方法簽名。
Ø
類別
在物件導向中,類別只是用來定義物件的實作細節而已。在類別中會訂定出物件的資料成員、方法所使用的演算法,以及其他種種的實作事項。換句話說,類別實現了物件的介面。
這裡要請注意的是,千萬不要把物件導向程式語言(Java、.NET、…等)中的interface和這裡本文中的介面搞混了。並不是程式語言中使用interface關鍵字定義的資料結構「才有介面」,而使用class關鍵字定義的資料結構「沒有介面」。這是一個物件導向程式語言所經常造成的誤解!物件導向中的介面是一個抽象的概念,凡是物件都一定具有本文所提的那種抽象的介面。
以下介紹類別的一些重要觀念:
n
繼承(Inheritance)
類別繼承被視為由一既存的類別定義出新的類別的方式。子類別會享受到父類別所開放的實作(資料成員與程式碼)。類別繼承的目的通常是為了重複使用既存程式碼(reuse),是重複使用的方式之一。
雖然類別繼承看起來非常的方便,但是過度使用類別繼承經常會產生巨大而混亂的繼承體系,而且父類別對子類別的繼承方式也經常有某種假設,更嚴重的是造成軟體無法更良好地因應需求的變動。所以在OOD中,反而偏好使用另一種重複使用的方式(後面會說明)—委託(delegation),以避免此類問題的發生。
n
抽象類別(Abstract Class)
許多物件導向程式語言都提供定義抽象類別的方式,但是初學OOP的人往往都感覺不到抽象類別的存在目的為何。有些人乾脆就把抽象類別視為擺放共用資料與程式碼的地方,然後以類別繼承的方式使用這些共用的東西。
事實上,抽象類別的用處不僅止於此。抽象類別的主要目的是為後代族系的具體類別定義一個共通的介面,讓物件的client端透過抽象類別來操控物件,避免client端直接與具體類別耦合,降低變動的修改難度。抽象類別通常會定義一些未實作的方法(稱為抽象方法),然後把部分或是全部的抽象方法留給後代子類別去實作。
在物件導向設計模式(design pattern)中,抽象類別佔有極重要的地位,幾乎在每一個模式中都會發現抽象類別的蹤影。
n
具體類別(Concrete Class)
這大概是物件導向中最為人們所熟悉的概念了。但是這也可能是許多人無法真正進入物件導向殿堂的原因之一。J
具體類別定義了真正的實作,它裡頭不會有任何的抽象方法。事實上物件就是實體化(instantiate)一個具體類別所產生的。換句話說,物件是具體類別的實體(instance)。
如果說抽象類別表達了系統中不變的部分,那具體類別就是系統中變化的部分。這個觀念在OOA與OOD中非常的重要。
具體類別常會覆載(override)父類別的一些方法,這就是所謂的多型(polymorphism)。
Ø
類別繼承v.s.介面繼承
類別繼承是用來以一個物件來定義另一個物件的實作細節;而介面繼承則描述了物件之間的可替換性。
在許多程式語言中,這兩個概念沒有被明顯的區分出來。在Java以extends和implements區別二者。Extends表示繼承某一類別的實作,而implements表示繼承介面而已。
所謂的「可替換性」表示在任何需要某特定介面的地方,只要傳入符合此特定介面的物件(亦即此傳入的物件繼承了此特定介面)即可。至於這個傳入的物件是否是同一類別並不重要。
Ø
類別繼承v.s.物件複合
物件複合(object composition)指的就是一個物件包含其他物件。當物件接受到client端的訊息(呼叫)後,將此訊息交付給其內含物件去處理。
就語意上的觀點來說,類別繼承表達了is-a的語意,而物件複合表達了has-a的語意。二者都是一種reuse的手法,只是類別繼承稱為白箱重複使用(white-box reuse),而物件複合稱為黑箱重複使用(black-box reuse)。
為何類別繼承稱為白箱重複使用呢?那是因為子類別若想繼承一個父類別時,通常子類別對父類別的一些實作方式必須有相當的瞭解,父類別對於子類別的繼承方式通常也有某種預期,這稱為父類別與子類別之間的合約(contract)。所以父類別對子類別來說是某種程度的「白箱」。
相反地,在物件複合機制中,物件只是將request轉交給內含物件處理。物件本身就如同內含物件的一個client端而已,它對內含物件實作方式的瞭解並不像類別繼承中父、子類別間那樣,所以內含物件好像是一個「黑箱」。
由於類別繼承中父、子類別之間的關係過於緊密,所以必須謹慎使用類別繼承機制。否則很容易造成系統的高度耦合。而物件複合則可避免此類問題。
所以請多使用物件複合,小心使用類別繼承。
Ø
委託
委託(delegation)指的就是當物件接受到client端的訊息(呼叫)後,將此訊息交付給其他物件去處理。這種機制在前面所提到的物件複合中也提到過。
在類別繼承所形成的多型中,父、子類別可以使用例如this或self等的關鍵字互相存取。但是在委託中,呼叫者物件必須將自己傳給被呼叫者物件,方能讓被呼叫者存取到呼叫者。
呼叫者物件通常面對的被呼叫者只是一個抽象類別或介面,呼叫者物件根本不知道真正被呼叫的物件為何。此與類別繼承不同,因為子類別知道父類別為何。
因為呼叫者與被呼叫者間對彼此實作沒有任何瞭解,所以委託機制通常用以降低物件之間的耦合度。
結語
Ø
物件導向設計原則
n
Programming to interface, not to implementation.
n
Favor objects composition over class inheritance.
Ø
難以因應需求變動的設計:
n
直接使用類別的名稱建立物件
n
與特定操作相依
n
與軟硬體平台相依
n
與物件佈局或實作方式相依
n
與演算法相依
n
高耦合度
n
過度偏重以繼承方式擴增功能
n
類別不易修改
沒有留言:
張貼留言