程式者的胡言亂語
- 全部
- Networking
- Travel
- Win32
- 黑米蟲
- programming
- DirectShow
- 書評
- 吃吃喝喝
- 站在Java的肩膀上看C#
- 音樂和電影
- General
- Java
化繁為簡,執簡御繁(下)
在前一回中,透過網路相簿下載程式的例子,更進一步的展示如何透過「捕捉事物的共通性,界定事物的相異性」來達到「執簡御繁」的目標。我們可以想像,倘若我們沒有試著去萃取出相簿下載功能的「核心」,我們的程式可能會像是這樣:
我們得分別實作下載這兩種網路相簿的程式碼-而這包含了許多形似且重複的部份。倘若我們試著整理出共通的核心:
其中共通的核心,我們只實作了一次,面臨不同的網路相簿的下載,我們僅增加了處理該相簿之網頁的方法。從圖形上看來,我們只不過是為同一個核上,裝上不同的殼(shell)罷了。爾後,每增加一種新的網路相簿,都只需要利用相同的核心(kernel),搭配新的殼就可以搞定。支援新網路相簿的力氣變小了,而且,當日後因應新的需求加強核心之後,只需要調整核心這一塊,完全不會發生必須要在許多重覆形式的程式碼中逐一修改的惡夢。此即本文所想要表達的重點所在,當你能夠透過「捕捉事物的共通性」的方式來建立起程式的核心時,就能夠進一步透過「界定事物的相異性」來為不同的需求建立起搭配核心一同使用的殼。核心表示的就是共通性,而每一份殼所實作的,便是不同需求之間的差異性。「化繁為簡」意謂著透過分析的手段,找出看似繁瑣的諸般變化的共通性,共通性就是化約後的純淨表示。我們可以將分析出來的共通性建構成單一的核心(例如各網路相簿下載時的共通行為),並且分別實作出所界定出來的變化方式(每種網路相簿都有不同取出相片網址的方式)。我們有力的掌握住最單純的部份,去駕御諸般不同的變化,這就是我所謂「執簡御繁」的意思。
捕捉共通性、界定相異性,是一種分析的手段。對於想要「執簡御繁」的我們,進行概念上的分析僅僅只是重要的第一步,在分析完成之後,還得有對應的語言機制足以讓程式員去表達這共通及相異。例如,在程序式(procedural)程式語言中,提供所謂的程序或函式,使得程式員得以將系統中具備共通性的程式予以抽離成為單獨的程序或函式,並透過搭配不同的參數,來表達每次執行該程序時的相異變因。我們看到在快速排序法及網路相簿下載程式的例子中,基本上便是採用這種最基本的語言機制來表示共通性(抽離出來的函式)及相異性(傳入不同的參數)。
這幾年來,許多程式設計上的觀念或程式語言的機制,其目的其實都在提供程式員實現「執簡御繁」的目標。好比C++所支援的參數化型別(也就是template),就是一種較程序式語言所提供之支援更強大的機制。使用程序式語言所提供的程序呼叫時,只能透過呼叫程序並傳入不同的引數值來表達「相異性」。但參數化型別,更進一步的將表達「相異性」的範圍,擴展到將型別做為參數的境界。還記得我們原先的qsort()是利用傳入的最後一個引數comp,來表示快速排序演算法應用時,在演算法共通性之類的型別相異性(不同的排序型別,有不同的比較方式)嗎?
void qsort(void *lineptr[], int left, int right,
int (*comp)(void *, void *));
倘若我們運用C++的template來實作這個函式,程式會變得更優雅:
template <class T>
void qsort(T lineptr[], int left, int right)
C++的template允許我們在撰寫類別或函式時,將其中所會需要一個或多個型別予以參數化。在這個參數化型別版的qsort()裡,我們直接令待排序的元素型別為一template的參數,利用了更直覺的方式表達這「相異性」。怎麼說它更直覺呢?在原來以函式指標做為引數的qsort()版本裡,只是利用了一個很高明、但又有點間接迂迴的來表示型別的相異性,因為其實它也只有表達出不同型別下的不同比較大小的方式罷了。但這種方式,純粹是土法煉鋼。我們可以看到,當應用情況更複雜一點,例如我們的網路相簿下載程式,就會需要兩個函式指標,這是因為變動的因子更多了。而且,隨著變動的因子愈多,這種技巧就會愈來愈不堪負荷,我們需要語言提供更直接的支援才行,而參數化型別便能允許你將所有可能會變動的因子,集合在個別的類別裡頭。倘若我們以參數化型別,重新改寫網路相簿下載程式的介面,我們可能會寫出如下的程式碼:
template <class T>
void downloadAlbum(URL *url, T *albumSpecialization)
{
URL *currentURL = url;
while( currentURL != NULL )
{
// ...
albumSpecialization->find_photo_url(/* 略*/ );
// ...
currentURL = albumSpecialization->find_next_page_url(/* 略*/ );
}
}
在網路相簿下載的例子中,我們找出了兩個會依著所下載相簿之不同而變動的因子,分別是在網頁中找出所含相片網址的方法,以及如何找出指向相簿下一頁網址的方法。這回我們不再使用函式指標的方式來表示,我們直接將這兩個因子定義在單一個類別裡,並將下載相簿的功能撰寫成一個具有參數化型別的function template,而此參數化的型別,正是封裝這兩個變因的類別。在這種寫法下,我們可以將處理網路相簿A的實作寫到AlbumASpecialization、將處理網路相簿B的實作寫到AlbumBSpecialization,依此類推。AlbumASpecialization的介面定義會像是這樣:
class AlbumASpecialization
{
public:
void find_photo_url(/* 略*/ );
URL * find_next_page_url(/* 略*/ );
};
搭配運用AlbumASpecialization及downloadAlbum()時,是這麼呼叫的:
downloadAlbum(startURL, new AlbumASpecialization());
有沒有發現,透過參數化型別的表示方式,整體的表述方式更簡潔而且直覺易懂呢?這就是語言是否有提供直接支援的差異所在。這正是我在本回一開始所提到的,這幾年來,許多程式設計上的觀念或程式語言的機制,其目的其實都在提供程式員實現「執簡御繁」的目標。語言上的支援是很重要的,因為倘若缺乏了語言上的支援,程式員即便能夠透過分析的技巧找出共通性及相異性,實作起來也會缺乏易於表達的工具,而淪於土法煉鋼,被迫使用較為原始的方式來達到相同的相同。在前一段中,我們看到了同樣的分析方式,但採用支援參數化型別的template的寫法,卻能更簡短、但又更直接明瞭的表示。這說明了語言本身的支援是相當重要的。
物件導向程式語言之所以能在這幾年取得關鍵的地位,無非是因為它充份的提供了允許我們「執簡御繁」的相關機制。在本系列中,持續的強調「捕捉事物的共通性,界定事物的相異性」是「執簡御繁」的關鍵。我們可以說物件導向程式語言的主要機制,有許多幾乎都為了支援這個精神而設計的。繼承是物件導向的重要概念,就讓我們從繼承談起。
在物件導向的概念裡,繼承是一個主要的支幹。在物件導向的術語裡,當B繼承A時,我們說A是一個一般化(generalization)的類別,而B是個特殊化(specialization)的類別。當C也繼承A時,C同樣的也成為一個特殊化的類別,而在這個時候,B及C有了共通的一般化類別A。倘若以class diagram表示,便有如下圖:
一般會用父類別及子類別的方式來稱乎A跟B以及A跟C之間的關係,這是因為物件導向採用了「繼承」這個語彙的關係。但事實上,使用「一般化」及「特殊化」的關係去理解,會更明白它實際的意涵。一個一般化的類別,代表它是它的所有特殊化的類別所共通的,它是比較general的。而每個隸屬於相同一般化類別的多個特殊化類別之間,彼此都有不同的特異之處,而這些特異之處正是它們在共通的一般化類別之外的額外展現。
有沒有注意到,當我們在設計時,運用了繼承的概念下,指定了類別和類別之間的一般化及特殊化關係時,正好就是我們在本系列中,一直強調的「捕捉事物的共通性,界定事物的相異性」的動作。當我們在設計的時候,決定了多個子類別所共有的父類別時,我們所做的正好是「捕捉事物的共通性」,而當我們決定子類別要如何的繼承父類別去展現自己所特有的行為時,所做的正好就是「界定事物的相異性」。從這個角度來看,就能夠明白這繼承機制的設計,幾乎就是為了滿足我們「執簡御繁」的目標所量身打造的。
繼承只不過是借用了生物分類的方式而命名。事實上,以一般化及特殊化的關係所建構而成的階層體系,正是人類學習、理解概念的一種很普遍的形式。例如生物學以界、門、綱、目、科、屬、種階層分類方式來為生物進行分類。在圖書館學中,也有體系十分完備的圖書階層分類方式。而像國際專利的公告,同樣有相當清楚的分類架構。下圖便是試舉一個電學專利分類的一部份,以class diagram加以表示:
從廣泛存在的分類體系,我們就能明白,其實這種階層性的分類方式,本來就是人類很自然的一種理解知識的方式。而在這種階層性的概念分類中,位於上層的,便是下層概念的共通概念,也就是較為一般化的概念。例如,對傳輸及廣播通信這兩個概念而言,它們的一般化共通概念就是電信通信技術。但對電信通信技術而言,還有一個更一般化的概念存在,也就是電學。在概念的階層分類體系中,愈往上層愈是一般化,愈是抽象,因為它代表的是底下所有概念的共通概念。而愈往階層分類體系的下方移動,概念就愈具象,所涵蓋的範圍就會愈來愈小。
物件導向的繼承機制,其實就是試著要去支援人類早已習以為常的知識分類方式。物件導向的精神在於,讓程式員試著以理解真實世界的方式,來描述解決真實問題的電腦系統。繼承機制的支援,就是希望讓程式員能夠更自然的運用以概念分類階層來理解真實世界的方式於軟體系統的開發。
不過,很可惜的是,許多使用物件導向程式語言進行開發的程式員,並沒有充份的意識到,在動用繼承的語法時,應該要指涉的,其實是一個一般化及特殊化的關係,而且沒有把握住「捕捉事物的共通性,界定事物的相異性」的這個中心思想。各物件導向程式語言的繼承語法都很單純,但困難的地方並不是在於語法本身,而是在於如何決定誰是一般化類別,那些類別又是特殊化的類別。一般化的類別需要具備那些特性,而特殊化的類別又分別多出那些特性。高手及庸手的分別,往往在此便可一顯無遺。
在本系列中持續的強調捕捉共通性、界定相異性是執簡御繁之道的核心關鍵。當我們在此處探討了繼承的機制之後,便應該能夠意識到,繼承機制本身,即是在提供這表達共通性及相異性的極佳支援。我們將共通性表示於一般化類別(也就是父類別),而在繼承的同時,透過覆蓋、變異、擴充等手段,於特殊化類別(也就是子類別)中,展現各特殊化類別的相異性。更有威力的是,前文僅使用到兩階層一般化、特殊化分析,而繼承機制允許我們層層相疊,藉以建構出一整個階層體系。
我們可以利用一個簡單的類別繼承,來重新設計網路相簿的下載程式,其中類別的介面(Java)會像是這樣:
public abstract class AlbumDownloader
{
abstract protected void findPhotoURL(/* 略*/ );
abstract protected URL findNextPageURL(/* 略*/ );
public void download(URL url)
{
URL currentURL = url;
while( currentURL != null )
{
// ...
findPhotoURL(/* 略*/ );
// ...
currentURL = findNextPageURL(/* 略*/ );
}
}
}
我們同樣的,把下載不同相簿的共通行為,抽離出來成為一個AlbumDownloader類別。在這個例子,我們更進一步的將它定義成為一個抽象的類別,為什麼這麼做呢?因為我們為它定義了兩個抽象的行為,分別是findPhotoURL()及findNextPageURL()。這正是我們在分析的過程中,所界定出來的相異性。當我們使用繼承來描述共通性及相異性時,共通性是在較抽象的父類別中表示,抽象的父類別並不知道繼承自它的子類別究竟會有什麼樣的具象展現,所以只能將可能會有的具象展現,定義成為抽象的methods,並且要求繼承自該父類別的子類別,都必須提供具體的展現方式,也就是實作這些methods。所以,當我們試著實作相簿A的下載類別時,我們會這麼寫:
class AlbumDownloaderA extends AlbumDownloader
{
protected void findPhotoURL(/* 略*/ )
{
}
protected URL findNextPageURL(/* 略*/ )
{
/* 略*/
}
}
對子類別而言,它不需要再重新實作各相簿下載程式中的共通部份,它只需要針對自己所獨特的、會有不同展現行為的這兩個methods進行實作即可。一旦我們有了新的需求,需要添加新的相簿下載程式,只需要依樣畫葫蘆,再繼承一個新的類別出來,並且實作這兩個methods即可。核心的、共通的AlbumDownloader,完全不需要改動,當然更不會需要重複地撰寫其中的程式碼。
有沒有看出來,在這個例子中,多型發揮了很大的作用。是的,當我們將共通的部份抽取出來成為核心的父類別,並將具體展現的責任交付給子類別時,扮演核心作用的父類別,雖然不知道子類別究竟會有什麼樣的具體展現(也就是說,不知道子類別會如何的實作這兩個抽象的methods),但它仍舊可以放心的去運用它們(請留意在AlbumDownloader中運用了findPhotoURL()及findNextPageURL()),因為這是繼承自父類別之子類別的責任。而這之所以能夠起作用,完全是基於背後的多型機制。
何謂多型?多型就是在同一個共通概念下,有不同的具體展現。我們所有的相簿下載程式都有著相同的下載方式,所以我們將這共通的下載方式,實作於父類別之中。但另一方面,不同相簿下載程式,也有其特殊之處,也就是擷取出網路中相片網址及下一頁相簿網址的方式不同。所以我們將這不同的具體展現,實作於子類別之中。侯捷先生在他的《多型與虛擬-物件導向的精髓》一書的標題中,很明白的表示出,物件導向語言,透過提供多型機制,讓我們更輕易的去操作一般化及特殊化的概念。透過多型,我們得以在共通的一般化概念中指涉可能因具體實現而有所不同的特殊化概念,但卻仍舊能夠表達出一個單一的核心。好比在相簿下載程式中,我們在核心的父類別AlbumDownloader中,能夠呼叫findPhotoURL()及findNextPageURL()這兩個在子類別中才會實作的methods一樣。雖然,這兩個methods實際上子類別會怎麼實作,父類別並不知悉,但是多型的機制,能夠使得父類別的這一段程式碼實際運作時,正確的以動態繫結的方式,呼叫到正確的子類別實作。
物件導向語言,為我們的「執簡御繁」之道,提供了很好的工具。物件導向中的繼承,允許我們將事物、概念的共通性、相異性,組織成為一個階層性的體系。而多型的機制,更允許我們在抽離出多種概念的共通性之後,輕易的將這共通性與可能的相異性融合在一塊。單純的只是透過繼承獲得父類別的現成程式碼,對程式碼重覆使用的效果並不突出,只不過像是呼叫現成的函式一樣。繼承真正的力量,在於與多型相搭配,將共通的概念描述於父類別,將具體的不同展現描述於子類別之中,而在父類別中操控這些可能會有的不同展現。
本系列始終貫穿的主軸就是,當我們能夠利用程式語言來做共通性及相異性的描述時,即便需求新增或變動,我們也能夠輕易的加以處理。倘若我們採用繼承及多型來描述系統的共通性及相異性時,當需求新增時,只需要撰寫新的子類別,去描述這新的需求就可以,好比我們想要增加一個新的網路相簿下載功能時,只需要實作一個不同的AlbumDownloader子類別即可。倘若我們的需求變更時,我們通常只需要針對描述共通性的父類別,進行修改即可。好比我們希望在下載相簿的同時,為下載下來的相片製作縮圖,只需要修改AlbumDownloader這個父類別,添加製作縮圖的步驟即可。我們所面臨的需求可能會持續變動,但是因為我們有著良好的設計,大半的擴充需求,只需要透過實作新的子類別就能夠完成。對於核心的修改或加強,只需要針對父類別進行修改或加強,就能夠辦到。我們所緊緊掌握的,就是這個很單純、夠抽象的單一核心,透過它(及多型的機制)搭配不同的子類別實作,就能夠輕易的面對這繁複且多變的需求。
本文至此已至尾聲。在這一系列中,我們看到了從函式指標的運用、參數化型別、到繼承與多型等各種不同的程式語言機制。要讓這些機制發揮最大的威力,必須搭配程式員在進行設計的同時,隨時觀注、分析自己所開發的系統中,那些組成彼此之間具備共通性,而各個組成在相同共通性的情況下,各又有那些相異性。能夠在思維上辦到這件事,才能夠進一步的透過程式語言的機制,來加以實現。尤其是參數化型別及繼承與多型,都讓我們得以更輕易的表達這樣思維後的產物。許多程式員並不是不明白繼承的語法,或者也明白多型的機制如何運作,但因為缺乏了在設計的過程中觀注系統中各組成之間共通性及相異性的動作,使得繼承或多型的機制,並不能發揮它應該要有的作用,而這是本文所試著要提醒的。想要輕鬆不費力的在惱人的需求變動過程中進行開發,本文建議你應該要從本文所建議的分析手法著手,再進行搭配程式語言的機制來實現,絕對能產生正面的效果。
(原文刊載於iThome)
Posted at 10:34上午 七月 25, 2007 by Chien-Hsing Wang in programming | 迴響[0]
化繁為簡,執簡御繁(上)
如果撰寫程式可以依境界高下分成很多個層次,那麼吾人雖然不見得有能力窺見最高的境界,但是可以確定的是,讓程式達成需求,只是這眾多高下不同的境界中最基礎的那一個層次。這樣的說法,或許令人感到迷惑,因為撰寫程式的目的,不就是為了達成系統的需求嗎?為什麼達成需求只是第一個層次呢?這是因為軟體系統的開發,有許多其他的因子必須考慮,這些因子有些會影響軟體系統本身的特性,也有些會影響到軟體系統開發專案的特性。例如,軟體系統的執行效能就是一個重要的因子,兩份同樣都是能夠達成功能需求的程式,在執行效能上的表現,卻可能有著天壤之別。又好比軟體開發的生產力,優良的程式技巧或方法與拙劣的程式寫法,其生產力有時往往不只數倍之差。這些眾多的因子,使得程式撰寫成了一門幾乎無限界的學問,而其中又充滿窮之無盡的藝術。
在程式撰寫的學問中,「化繁為簡,執簡御繁」是許多程式員所努力追求的目標。我們所開發的系統,其中往往充滿著各式各樣看似十分繁雜多變的需求。但這些看似繁雜多變的需求中,需求與需求之間本質上卻又往往有共通之處,倘若能夠掌握這些共通的部份,是否就能更輕易的掌握這多變的需求呢?
對於前段所言,我曾在C語言的聖經本”The C Programming Language 2nd Edition”中看到一個很經典的例子,它是一個關於快速排序法(Quick Sort)的例子。我們只需要檢視其函式的原型,毋需觀看整個函式的定義,就能夠明白其中的奧妙之處,它是這樣子宣告的:
void qsort(void *lineptr[], int left, int right,
int (*comp)(void *, void *));
lineptr是個陣列,儲放要被排序的標的物,left及right分別是要被排序的陣列索引值範圍,而comp是個函式指標,稍後我們再來說明它的作用。
剛入門的程式員倘若被交付任務,要撰寫一個快速排序法的函式,他可能會這麼宣告此函式的原型:
void qsort(int lineptr[], int left, int right);
這麼一來,這個函式便能針對整數值陣列進行排序。很不巧的,他接著又被告知,我們還會需要一個針對浮點數做快速排序的函式,所以呢,這位程式員,只好祭出拷貝並複製(copy & paste)的絕技,將已經寫好的整數快速排序,複製成另一份,把int稍加更動一下成為float同時更名:
void qsort_f(float lineptr[], int left, int right);
如果那天需要針對新的型別再排序,便如法泡製:
void qsort_foo(foo lineptr[], int left, int right);
這樣看似解決問題,但爾後隨著需求不斷的湧現,需要做快速排序的型別愈來愈多,套用同樣的方式的確能夠解決需求端的問題,但也衍生出其他的問題。這樣的問題主要源自於兩大因素,第一個是重複的程式碼所造成的維護問題,第二個則是無法大幅提昇生產力的問題。
當我們的系統中充斥著重複形式的程式碼時,倘若要修改這段程式碼(基於發現了這段程式有臭蟲、或者基於想要加以擴充的需求),你會發現不容易在整個龐大的code base中找出這些形式類似的程式碼片段,修改難免有漏網之魚。即使完全找出所有衍生自同一段程式碼的程式碼片段,也會面臨修改不易的困境,複製過的片段愈多,修改起來愈耗力。
再者,複寫再修改重複形式的程式碼也許能稍微提昇開發的生產力,畢竟基於某個已經存在的程式碼片段進行修改,也可以節省重頭開始撰寫的時間。但是,即便如此,程式員仍得費心的檢查,目前需要的程式碼,與做為範本的程式碼之間,究竟存在什麼樣的差異,並且在修改時套用這樣的差異。在這過程中,仍然有許多時間被消耗掉了。
我們回頭看看聖經本上的經典範例,在這個範例中,lineptr被宣告為void *,它根本就不去侷限被排序的型別究竟為何,使得它可以被套用在任意型別的排序!在前文中,我們並沒有寫下qsort()的實際定義,但是在這個函式中,隨著型別而變的部份只有一個,就是比較兩個值的大小。對於整數,我們可以寫死為整數的>及<運算子;對於浮點數,我們可以寫死為浮點數的>及<運算子,但對於不可預測的型別呢?聖經本的經典範例利用了函式指標comp來達成。通用性質的快速排序,在遇到需要比較兩值的大小時,便會呼叫comp函式指標並傳入欲比較的兩個值,由此函式指標來告訴快速排序的演算法比較的結果為何。這麼一來,快速排序演算法的撰寫,便和排序的型別完全脫離關係。只要撰寫一份qsort(),之後要針對不同的型別進行排序,只要為該型別提供一份比較函式的實作,屆時傳入指向該函式的指標進入qsort()即可,想要為新的型別進行排序,十分的快速,生產力可藉此提昇。倘若,發現了qsort()的實作有問題或需要加以擴充,只需要修改這唯一的一份qsort(),其餘呼叫qsort()來針對不同型別進行排序的程式碼,完全不需要更動,重複程式碼在維護上的問題也完全消失。
有人說,”The C Programming Language 2nd Edition”裡的範例個個都經典到不行,一個例子可以抵的上十個例子。這個例子雖然是拿來說明函式指標的作用,但卻在無形間點出「化繁為簡,執簡御繁」之道。
我們需要針對五花八門的型別進行快速排序,是可以看到的繁複表象,但在這繁複的表象之中,有一個很簡單的核心,就是快速排序的演算法本身。倘若我們能夠像這個例子中一樣的「化繁為簡」,那麼便能夠憑藉著既單純又簡化的核心,「執簡御繁」。
那麼,不禁要問的是,要如何才能「化繁為簡」以進一步達到「執簡御繁」呢?那就請見下一集分曉。
(原文刊載於iThome)
Posted at 12:22下午 七月 06, 2007 by Chien-Hsing Wang in programming | 迴響[1]
評論 :關於寫程式需不需要用到數學
最近Mr./Ms. DAYS blog貼出一篇標題為「寫程式到底需不需要數學?」的文章。其實,這樣子的討論,一直都是早年台灣BBS的programming版面的週期性主題(又稱月經題)。這樣的討論裡,一定會有人跳出來說,我數學不好,程式還不是寫的嚇嚇叫。但也一定會有人跳出來說,如果你數學不好、演算法不好,到時寫出來的程式效能要多差就會有多差之類的。所以,這樣子的爭論常常就會沒有什麼結果,因為大家各執一詞,都有各自的說法。
先說為什麼有人「數學」不好,但程式還是寫的嚇嚇叫。這種情況是因為他並不需要開發需要「特定數學領域知識」的系統。倘若你要開發的系統,需要做高度的數值運算,那麼沒有數值運算背景,系統當然可能就跑不快。不熟悉矩陣的運算技巧,做起需要電腦圖學的系統,當然會出問題。可這道理就和你要開發一個會計系統,你不懂會計的領域知識是一樣的道理。
數學在這種情況下,只是一種領域知識。
對系統開發來說,領域知識固然是整個開發團隊中需要獲取或具備的重要元素,但除此之外,重要的元素還有很多。是否有好的開發流程、軟體工程能力、對語言、開發模式的熟悉、軟體架構設計的能力等等。很多人開發系統根本不需要用到特定領域的數學,或者是團隊中跟本就有數學領域專家,自然就認為程式寫的好不好和數學的關係不大。畢竟現代軟體系統的開發,已經集團作戰而非單兵作業的年代,對程式員而言,多能妥善幾個好的設計模式,搞不好還更有用。我看過一個醫院健保申報最佳化系統開發的例子,在該團隊中,有好的程式員,但也有一位好的數學本科成員。在他們的應用中,倘若使用暴力法,尋求最佳解的情況,會耗去十分漫長的時間。但有數學背景果然不同,他找出了一個方法可以剛好找到最佳解,雖然有數學背景的這位成員他的programming能力並不好,但在這樣的組合下,就開發出一個比起競爭對手來說,效能有截然不同表現的系統。這當然是特定領域數學的重要性,但無論如何,在這種情況下,特定領域數學也是做為一種領域知識,它可以不必是程式員的專業,好比薪資系統裡要如何結算薪資一樣,都是一種領域知識。
看到這邊,或許會有錯覺,覺得我是站在「寫程式不需要用到數學」這個陣營裡的人吧。如果這麼想,也就猜錯了,恰好相反。我認為系統設計及撰寫程式的過程無處不用到數學。
也許會留意到,我在前幾段的文字中,一直強調的是「特定領域」的數學。是因為這些數學技巧,都是為了解決領域問題所需要的。但如果把數學看的這麼窄,不免失去核心的所在。
我認為數學能力有更基礎的部份,我們在解題時運用的數學技巧包括:分類、歸納、演繹、推理、推論、分析、變形、一般化、特殊化等等。
時下最流行的語言類型,莫過於物件導向程式設計了。有人認為物件導向設計的三大特質是資料封裝、繼承及多型。要將物件導向應用的好,其實需要廣泛的運用分類、歸納、變形、一般化、特殊化等等技巧。繼承階層體系的設計是分類,找出base class及derived calss之間的關係是變形、一般化、特殊化的能力。想要善用多型,更是必須將一般化及特殊化的能力操作到十分流暢才行。但這些能力,卻是我們在討論寫程式究竟需不需要數學時,時常會加以忽略的。
好的軟體設計者,若要提高生產力、降低出錯的機會、擴展系統的擴充彈性,更不可能避掉上述提到的種種數學的根本解題能力,因為只有妥善的能用它們,才有可能設計出好的架構滿足上述的需求。事實上,能掌握上述的數學能力,除了談戀愛外,還有什麼事是不能夠做起來得心應手的呢?
Posted at 11:49上午 五月 31, 2007 by Chien-Hsing Wang in programming | 迴響[1]
評論 :關於語言的效能或scalability
最近因為採用RoR開發的Twitter遇上scalability的問題,所以導致開發者「更加的」關注RoR的scalability問題。之所以說是「更加的」,是因為原先大家都公認RoR的效能表現並不夠好,對於它在效能的問題上一直有存疑,而爆紅的Twitter正好可以提供一個例子,讓大家探索RoR相關技術目前在scalability上的極限究竟為何。
我的看法是,或許目前RoR在效能或scalability上的表現不好是事實,不過這有很大的原因是因為它還是一個不那麼成熟的技術領域。許多Java的開發者都一路渡過從JDK 1.0開始的時代,那時的JVM既慢、bug又多。但隨著時間的過去,不論是編譯器或VM的技術都持續成熟,現今的JVM可以說是一代比一代更快、效能更好。
所以,除非有一些根本上的架構錯誤,不然我會很樂觀的相信實作技術能夠改善效能的問題。
我認為,我們在思考程式語言或framework時,應該要考慮的是模式(paradigm)的正確與否以及抽象化的程度。模式代表的是一種表達陳述的模式,好的表達模式,能提昇生產力、減少錯誤、易於擴充及維護。我最近常聽lukhnos提到RoR的「謎之一行」,表示一行抵千行的效用。為什麼一行程式碼的表述能夠抵的上千行程式的效果,這意謂著很高的抽象化程度。每種語言及framework都位在某種位階的抽象化程度之上,尤其framework更是對特定應用的抽象化及一般化。有生產力的framework都是抽象化程度高的,否則是無法有所謂的「謎之一行」。
高抽象化的好處是效能改善空間大。當client programmer的表述愈精簡時,代表大部份的工作都是語言或framework處理掉,只要加強在語言或framework的內部實作,它們的效能改善了,程式的效能就改善了。好比新版的JVM提昇了效能,原有的程式在新的JVM上執行,效能表現就能更好一樣。參與語言或framework設計及實作的都是專家,讓專家調校語言或framework實作,是一個比較好的方式。而表述愈是精簡,效能愈有改善的空間。
RoR現階段看起來,應該有正確的paradigm,framework也有生產力,我相信就算效能不彰,改善也是遲早的事。最怕的往往不是效能不好,而是paradigm不對,導致整個開發生產力低落,那可就從根本上就不對,而不是「遲早」二字所能解決的。
Posted at 11:02上午 五月 31, 2007 by Chien-Hsing Wang in programming | 迴響[8]
Internet Explorer加上proxy的怪事
今天解了一個懸案。當瀏覽器如果是Internet Explorer 6.0(包含)以前的版本的話,而且又是透過proxy連接到Web application時,Internet Explorer是不會送出Accept-Encoding的這個request header的。所以,倘若在Web applicaiton端沒有老老實實的檢查Accept-Encoding,就直接的送出含Content-Encoding的response header時,那Internet Explorer的反應就不會對了。出現的行為,大致上就是popup一個視窗,問你要存檔還是要開啟之類的。
這問題FireFox是沒有的啊。FireFox不管有沒有接到proxy,都能夠處理response有Content-Encoding的情況。但唯獨Internet Explorer就是在連接proxy時會有這個特性。我想,應該跟Internet Explorer在處理Vary header時的問題有關,我找到這篇文章: MSIE cannot handle Vary header(s) 。
結論是,做人還是不要太懶的好 ,一切還是應該要照標準來。
Posted at 06:36下午 五月 17, 2007 by Chien-Hsing Wang in programming | 迴響[2]
星期三 七月 25, 2007
