前一回,我們透過網路相簿下載程式的例子,展示如何透過「捕捉事物的共通性,界定事物的相異性」來達到「執簡御繁」的目標。倘若沒有試著萃取出相簿下載功能的「核心」,我們的程式可能得分別為兩種網路相簿實作下載程式,這中間包含了許多形似且重複的部分。
倘若我們試著整理出共通的核心,那麼共通的核心,我們只需實作一次,面對不同的網路相簿,我們僅需處理存取該相簿網頁的方法。
從圖形上看來,我們只不過是為同一個核心(Kernel),裝上不同的殼(Shell)罷了。日後,每增加一種新的網路相簿,都只需要利用相同的核心,搭配新的殼就搞定了。那麼,支援新網路相簿的力氣變小了,而且,當日後因應新的需求加強核心之後,只需要調整核心,可以避免在許多重複形式的程式碼中,逐一修改的惡夢。
此即本文所想要表達的重點,當你能夠「捕捉事物的共通性」建立起程式的核心時,就可以進一步透過「界定事物的相異性」,為不同的需求建立起搭配核心一同使用的殼。
核心所表示的特質就是共通性,而殼所實作的,便是不同需求之間的差異性。「化繁為簡」意謂著透過分析的手段,從看似繁瑣的諸般變化找出共通性,簡單地說,共通性就是化約後的純淨表示。
我們可以將分析出來的共通性建構成單一的核心(例如各網路相簿下載時的共通行為),並且分別實作出所界定出來的變化方式(每種網路相簿都有不同取出相片網址的方式)。我們有力地掌握住最單純的部份,並駕御諸般不同的變化,這就是所謂「執簡御繁」的意思。
捕捉共通性、界定相異性,是一種分析的手段。對於想要「執簡御繁」的我們,進行概念上的分析,僅只是重要的第一步,在分析完成之後,還得有對應的語言機制,足以讓程式員表達共通及相異。
例如,在程序式(Procedural)程式語言中,提供所謂的程序或函式,使得程式員得以抽離系統中共通的程式,成為單獨的程序或函式,並透過不同的參數,表達每次執行該程序時的相異變因。
我們看到在快速排序法及網路相簿下載程式的例子,基本上便是採用這種最基本的語言機制,以表示共通性(抽離出來的函式)及相異性(傳入不同的參數)。
這幾年來,許多程式設計上的觀念,或者是程式語言的機制,目的都在提供程式員實現「執簡御繁」的目標。好比相較程序式語言提供的支援,C++所支援的參數化型別(也就是Template),就是一種更強大的機制。
使用程序式語言所提供的程序呼叫時,只能透過呼叫程序並傳入不同的引數值來表達「相異性」。但參數化型別更進一步地把「相異性」的範圍,擴展到視型別為參數的境界。還記得我們原先的qsort()是利用傳入的最後一個引數comp,表示快速排序演算法應用時,在演算法共通性之類的型別相異性(不同的排序型別,有不同的比較方式)嗎?
倘若我們運用C++的template實作這個函式,程式會變得更優雅:
C++的template允許我們在撰寫類別或函式時,將其中所會需要一個或多個型別予以參數化。在這個參數化型別版的qsort()裡,我們直接令待排序的元素型別為一template的參數,利用了更直覺的方式表達這「相異性」。
怎麼說它更直覺呢?在原來以函式指標做為引數的qsort()版本裡,只是利用了一個很高明、但又有點間接迂迴的方式表示型別的相異性,因為其實它也只有表達出不同型別下的不同比較大小的方式罷了。
但這種方式,純粹是土法煉鋼。當應用情況更複雜一點,例如我們的網路相簿下載程式,就會需要兩個函式指標,因為變動的因子更多了。隨著變動的因子愈多,這種技巧就會愈來愈不堪負荷,我們需要語言提供更直接的支援,而參數化型別便能允許你將所有可能會變動的因子,集合在個別的類別裡頭。倘若我們以參數化型別,重新改寫網路相簿下載程式的介面,我們可能會寫出如下的程式碼:
在這種寫法下,我們可以將處理網路相簿A的實作寫到AlbumASpecialization、將處理網路相簿B的實作寫到AlbumBSpecialization,依此類推。AlbumASpecialization的介面定義會像是這樣:
搭配運用AlbumASpecialization及downloadAlbum()時,是這麼呼叫的:
你發現了嗎?透過參數化型別的表示方式,整體的表述方式更簡潔而且直覺易懂。這就是語言是否提供直接支援的差異所在。
語言的支援是很重要的,因為倘若缺乏,程式員即便能夠透過分析的技巧找出共通性及相異性,實作也會缺乏易於表達的工具,而淪於土法煉鋼,被迫使用較為原始的方式,才能達到相同的目的。
物件導向程式語言之所以能在這幾年取得關鍵的地位,無非是因為它充分提供允許我們「執簡御繁」的相關機制。
在物件導向的概念裡,繼承是一個主要的支幹。在物件導向的術語裡,當B繼承A時,我們說A是一個一般化(Generalization)的類別,而B是個特殊化(Specialization)的類別。當C也繼承A時,C同樣地也成為一個特殊化的類別,而B及C有了共通的一般化類別A。我們可以用類別圖(Class Diagram)表示。
一般會用父類別及子類別的方式,稱呼A跟B以及A跟C之間的關係,這是因為物件導向採用「繼承」這個語彙的關係。但事實上,使用「一般化」及「特殊化」的關係理解,會更明白實際的意涵。
一個一般化的類別,代表它是所有特殊化的類別所共通的,它是比較「General」的。而每個隸屬於相同一般化類別的多個特殊化類別之間,彼此都有不同的特異之處,而這些特異之處,正是它們有別於共通的一般化類別的額外展現。
當我們在設計時,運用繼承的概念,指定了類別和類別之間的一般化及特殊化關係時,正好就是我們一直強調的「捕捉事物的共通性,界定事物的相異性」。
繼承只不過是借用了生物分類的方式而命名。事實上,以一般化及特殊化的關係所建構而成的階層體系,正是人類學習、理解概念的一種普遍形式。例如生物學以界、門、綱、目、科、屬、種的階層分類方式為生物分類。
在概念的階層分類體系中,愈往上層愈是一般化,愈是抽象,因為它代表的是底下所有概念的共通概念。而愈往階層分類體系的下方移動,概念就愈具象,所涵蓋的範圍就會愈來愈小。
物件導向的繼承機制,其實就是試著支援人類早已習以為常的知識分類方式。而且物件導向的精神在於,讓程式員試著以理解真實世界的方式,來描述解決真實問題的電腦系統。繼承機制的支援,希望程式員能夠更自然地運用概念分類階層,以真實世界的方式理解軟體系統的開發。
不過,很可惜的是,許多使用物件導向程式語言的程式員,並沒有充分的意識這個概念,在動用繼承的語法時,應該要指涉的,其實是一般化及特殊化的關係,而且都沒有把握住「捕捉事物的共通性,界定事物的相異性」的中心思想。
各物件導向程式語言的繼承語法都很單純,困難的地方並不在於語法本身,而是如何決定誰是一般化類別,那些又是特殊化的類別。一般化的類別需要具備的特性,而特殊化的類別分別又多出的特性是很重要的。高手及庸手的分別,往往在此便可一顯無遺。
更有威力的是,前文僅使用到兩階層的一般化、特殊化分析,而繼承機制允許我們層層相疊,藉以建構出一整個階層體系。
我們可以利用一個簡單的類別繼承,重新設計網路相簿的下載程式,其中類別的介面(Java)會像是這樣:
這正是我們在分析的過程中,所界定出來的相異性。當我們使用繼承描述共通性及相異性時,共通性是在較抽象的父類別中表示,抽象的父類別並不知道繼承自它的子類別究竟會有什麼樣的具象展現,所以只能將可能會有的具象展現,定義成為抽象的Methods,並且要求那些繼承自該父類別的子類別,都必須提供具體的展現方式,也就是實作這些Methods。所以,當我們試著實作相簿A的下載類別時,我們會這麼寫:
對子類別而言,它不需要再重新實作各相簿下載程式中的共通部分,只需要實作獨特的、會有不同展現行為的Methods即可。一旦我們有了新的需求,需要添加新的相簿下載程式,只需依樣畫葫蘆,再繼承一個新的類別,並且實作這兩個Methods即可。
在這個例子中,有沒有看出來多型發揮了很大的作用?是的,當我們將共通的部份抽取出來成為核心的父類別,並將具體展現的責任交付給子類別時,扮演核心作用的父類別,雖然不知道子類別究竟會有什麼樣的具體展現(也就是說,不知道子類別會如何的實作這兩個抽象的Methods),但它仍舊可以放心地運用它們(請留意在AlbumDownloader中運用了findPhotoURL()及findNextPageURL()),因為這是繼承自父類別的子類別所背負的責任。而這之所以能夠起作用,完全是基於背後的多型機制。
何謂多型?多型就是在同一個共通概念下,有不同的具體展現。我們所有的相簿下載程式都有著相同的下載方式,所以我們將這共通的下載方式,實作於父類別之中。
但另一方面,不同相簿的下載程式,也有特殊之處,也就是擷取出網路中相片網址及下一頁相簿網址的方式不同。所以我們將這不同的具體展現,實作於子類別之中。
侯捷在他的《多型與虛擬-物件導向的精髓》一書的標題中,很明白地表示,物件導向語言透過提供多型機制,讓我們更輕易地操作一般化及特殊化的概念。透過多型,我們得以在共通的一般化概念中,指涉可能因具體實現而有所不同的特殊化概念,但卻仍舊能夠表達出一個單一的核心。
需求變更時,只需要修改描述共通性的父類別。好比在下載相簿的同時,如需為下載的相片製作縮圖,只需要修改父類別,添加製作縮圖的步驟即可。
需求可能會持續變動,但是因為有著良好的設計,大半的擴充需求,只需要透過實作新的子類別就能夠完成。對於核心的修改或加強,只需要針對父類別進行修改或加強。我們所緊緊掌握的,就是這個很單純、夠抽象的單一核心,透過它及多型的機制,搭配不同的子類別實作,就能夠輕易面對這繁複且多變的需求。
能夠在思維上辦到這件事,才能夠進一步透過程式語言加以實現。尤其是參數化型別及繼承與多型,都讓我們得以更輕易地表達思維後的產物。許多程式員並非不明白繼承的語法,也明白多型的機制,但缺乏在設計的過程中關注系統各組件之間的共通性及相異性,使得繼承或多型的機制,並不能發揮應有的作用。
想要輕鬆不費力地在惱人的需求變動過程開發,建議你應該要從分析的手法著手,再搭配程式語言的機制實現,絕對能產生正面的效果。
《作者簡介》王建興
清華大學資訊工程系的博士研究生,研究興趣包括電腦網路、點對點網路、分散式網路管理、以及行動式代理人,專長則是Internet應用系統的開發。曾參與過的開發專案性質十分廣泛而且不同,從ERP、PC GAME到P2P網路電話都在他的涉獵範圍之內。
熱門新聞
2025-01-26
2025-01-25
2025-01-26
2025-01-27
2024-04-24
2025-01-24