如果撰寫程式可以依境界高下分成很多個層次,那麼吾人雖然不見得有能力窺見最高的境界,但是可以確定的說法是,「讓程式達成需求」只是這眾多高下不同的境界中最基礎的那一個層次。這樣的說法,或許令人感到迷惑,因為撰寫程式的目的,不就是為了達成系統開發的需求嗎?

為什麼達成需求只是第一個層次呢?這是因為軟體系統的開發,有許多其他的因子必須考慮,這些因子中有的會影響軟體系統本身的特性,也有的會影響到軟體系統開發專案的特性。例如,軟體系統的執行效能就是一個重要的因子,同樣都是能夠達成功能需求的兩支程式,在執行效能上的表現,卻可能有著天壤之別。又好比軟體開發的生產力,優良與拙劣的程式技巧或方法,它們的生產力有時往往不只數倍之差。這些眾多的因子,使得程式撰寫成了一門幾乎無限界的學問,而其中又充滿窮之無盡的藝術。


在《The C Programming Language 2nd Edition》中,關於快速排序法(Quick Sort)的段落正是「執簡御繁」的典型例子。

在程式撰寫的學問中,「化繁為簡,執簡御繁」是許多程式員所努力追求的目標。我們所開發的系統,通常充滿著各式各樣看似十分繁雜多變的需求,但往往需求與需求之間本質上卻又有共通之處,倘若能夠掌握這些部份,是否就能更輕易處理這多變的需求呢?

對於前段所言,我曾在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 Editio》裡的範例個個都經典到不行,一個例子可以抵得上十個例子。這雖然是拿來說明函式指標的作用,卻恰好在無形間點出「化繁為簡,執簡御繁」之道。

我們需要針對五花八門的型別進行快速排序,這是可以看到的繁複表象,但在繁複的表象之中,有一個很簡單的核心,就是快速排序的演算法本身。倘若我們能夠像這個例子中一樣的「化繁為簡」,那麼便能夠憑藉著既單純又簡化的核心,「執簡御繁」。

那麼,不禁要問的是,要如何做才能「化繁為簡」,以進一步達到「執簡御繁」的效果呢?就請見下一集分曉。

《作者簡介》王建興
清華大學資訊工程系的博士研究生,研究興趣包括電腦網路、點對點網路、分散式網路管理、以及行動式代理人,專長則是Internet應用系統的開發。曾參與過的開發專案性質十分廣泛而且不同,從ERP、PC GAME到P2P網路電話都在他的涉獵範圍之內。

熱門新聞

Advertisement