文藻外語大學W-Portfolio

超乎想像的心世界

2010-03-01 14:05:50

程式設計的兩個觀點(轉載)

程式設計的兩個觀點


程式語言PASCAL之父Niklaus Wirth曾經說過 ”Algorithms + Data Structures = Programs”。倘若程式是由演算法及資料結構所組成的,那麼設計程式的活動,似乎也應當是由演算法及資料結構的設計及運用所支配著。

許多程式員們都曾修習過「資料結構」以及「演算法」的課程。但說實話,肯定有一定比例的程式員在離開校園投入職場後,往往會發現在自己實務的開發經驗中,在這兩門課程中所學的,「似乎」都很少發揮作用。我相信,對很多程式員來說,心中難免存著疑惑,許多人都說這兩件事情對程式設計來說十分的關鍵,那為什麼在自己實務的開發經驗中,很少感受到到它們的重要性呢?可是,會不會也時常聽到別人說,倘若你不懂資料結構、甚至是演算法,那麼你不過只是一個撰寫程式碼的工人,而無法成為一個有價值的程式員?對此,你是否曾經感覺到憂心或是迷惑呢?

事實上,隨著電腦科學的發展,許多有用的資料結構及演算法,早已發展到十分成熟的境界。同時,這些資料結構以及演算法,也已經被廣泛的實作並且以各種對程式員來說的高階形式包裝。在學校修讀資料結構及演算法課程時,教科書也許花上相當多的篇幅討論排序演算法,你或許會學到Bubble Sort、Insertion Sort、…、Merge Sort、Quick Sort等等排序的演算法。但當你投入真正的程式設計工作時,你會發現教科書上所介紹的演算法都有現成的程式庫提供實作,例如C語言的標準函式庫中的qsort()函式即為提供Quick Sort演算法的函式。對開發工作來說,需要另行設計出自有的排序演算法,其機會實在是微乎其微。

談到了排序,讓我們來看看一個值得一提的例子吧。Java標準程式庫中,有個叫做java.util.Arrays的類別,提供了一系列重載版本的sort()函式,允許你對各式型別的物件陣列進行排序,甚至允許你自訂物件在排序時的比較方式,是個相當通用而且威力十足的排序函式。

你也可以和我一樣翻開手邊的JDK原始碼。我手邊這個類別的.java檔原始碼版本是1.59,掛名作者的,正是2006年造訪台灣、現正任職Google的兩位大師-Josh Bloch以及Neal Gafter。如果你找出sort()函式的說明,原始碼中明白的指出,其排序演算法乃是採用一篇標題為 ”Engineering a Sort Function” 的論文中所提出、經調適後更為快速的Quick Sort演算法。如果你更進一步檢視,你會發現當欲排序的元素個數小於7時,它便不再使用Quick Sort,而是採用Insertion Sort。

即使是像這兩位卓越的大師,在他們實作排序的功能時,並沒有自行設計出新的演算法,而是採用前人所發展的演算法。這並不損這兩位大師的價值。這個演算法並不是出自於一般教科書中所介紹的演算法,而是出自於學術期刊上所發表的論文???即使你熟讀教科書中的Quick Sort,也不見得能自行發展出如此快速的Quick Sort。而且,在Java標準程式庫中的這個實作中,更是兼顧了實務的考量,因為在元素個數很小時,時間複雜度為n^2的Insertion Sort,實際執行速度還勝過號稱平均會有n*log(n)的Quick Sort。

對現在的程式員來說,演算法的影子在設計工作中之所以愈來愈淡,有一個核心的原因,便是在於我們所需求的絕大多數各種演算法,早已在層層的分工下被發展的十分完備。有人發展原始的演算法、有人持續的進一步改進、有人找到實務上調校的技巧、有人以高階的形式加以包裝,最終成了可供客戶端程式員直接運用的產物。於是,到了程式員手上,只消呼叫Arrays.sort(),排序問題就被神燈中冒出來的巨人神奇般的解決了。

排序演算法在此僅僅只是一個例子,但不可否認的,這數十年整個電腦科學學術界的研究成果,早已將能滿足我們需求的各種演算法和資料結構,以相當高階的形式包裝起來,對程式員來說唾手可得。這使得程式員即使完全不懂得這些演算法及資料結構的細部運作,仍然有可能如魚得水的加以操控。事實上,不僅僅是演算法領域如此,整個電腦科學領域的研究成果,皆在此類層層的分工下,為程式員提供既高階且便利的工具。

所以,你或許會留意到,現今的程式員似乎更為重視在演算法及資料結構以外的東西。例如,程式員們更為重視架構上的「設計」。這設計並非演算法或資料結構的規劃及設計,而是規劃、安置軟體建構區塊之間的連結及關係。從演算法、資料結構出發的程式設計觀點,其眼光放在如何讓計算機器以有高執行效率、節省資源的方式來解決特定的問題。但從架構觀點出發的程式設計觀點,卻是著重在如何以有彈性、容易擴充、容易改變的方式,迅速的組裝搭配各種建構區塊,並從中獲取最大的生產力。

這兩種程式設計的觀點,呈現出截然不同的面貌,也時常的惹來喋喋不休的爭論。從演算觀點來看程式設計的人,會認為不懂演算法、或不知如何設計演算法的程式員,設計不出高效、節省資源的程式,所以自然缺乏價值。著重架構觀點的人,也同樣的認為許多擅長解題的人、不擅長設計出架構上具有優勢的程式碼。

這中間的認知衝突,乃是源自於程式設計本身多面向的特質,程式設計既有演算的面向,也同樣的具有架構的面向。程式設計也因這多面向的特質,造成了盲人摸象的情況。站在演算觀點的人說程式設計就是演算法加上資料結構,而立足於架構觀點則說,程式設計與建造建築物差可比擬。對同樣的一項活動,竟然會有差別這麼大的描述方式。

早期對程式設計的活動,偏重在解決特定的問題。這是因為早期對電腦程式的需求,皆起因於想要解決特定的演算問題。設計者可能希望計算給定一組城市以及城市間的距離,計算出任兩個城市之間的最短距離,於是我們有了最短路徑的問題。隨著各式演算問題被探索、研究。這些資產逐漸的被累積,便成了現今程式員得以垂手便取的現成產物。這些產物不僅現成,而且還不容易被超越。

當程式員愈來愈不需要自行開發、實作解決各式特定問題的程式碼時,程式員手上有的,便是許許多多的建構組件,這些組件的來源,正如前段文字所述,乃是以多年的研究探討為基礎,再加上軟體產業的層層分工而來的。能解決問題的組件多半毋需重新再造輪子,況且自己再造的輪子其品質也難以和現成輪子匹敵,那麼很自然的,程式員便會將重心逐漸轉移到如何更有生產力的裝配這些組件。這些組件對程式員來說,都像是完全不透明的黑盒子一樣,程式員毋需理會其內部實作,只需要從抽象高階的觀點來理解這些組件,並妥善的加以運用即可。一時之間,看似架構觀點所重視者,其重要性又凌駕於演算觀點所重視者。

程式設計是一種多面向特質的活動,既可以從演算的觀點來看待,也可以從架構的角度來加以檢視。從演算的觀點來看,程式設計的焦點在於如何讓計算機器以有高執行效率、節省資源的方式來解決特定的問題。而架構觀點則重視如何以有彈性、容易擴充、容易改變的方式,迅速的組裝搭配各種建構區塊,並從中獲取最大的生產力。

這兩種角度都反映出某種程度程式設計活動的本質,但卻又都不完全。所以我們時常會看到一個現象,有些程式員信仰程式設計的演算觀點,往往認為演算法才是程式設計的王道。對他們來說,程式的時間複雜度,或具體的執行效率是凌駕於一切的指標。程式的美感便於此處展現。信仰架構觀點的程式員,重視則是軟體架構上的特性,例如可擴充性、彈性、生產力、易於理解、…等等。對他們來說,程式的美感,有著不同的定義方式。

通常在校園中,電腦科學的程式設計課程多半偏重演算觀點的角度。但是,一旦進入職場後,似乎軟體架構的重要性又在這演算能力之上。當然,這並無法以偏概全,有些人的程式設計工作,便是在發展新的演算法,例如從事電腦輔助IC設計系統的人。又例如像開發多媒體編解碼器的程式員,其工作對於效能輜銖必較,非得千方百計的壓榨電腦的每一分力氣。對這類程式員來說,演算觀點自然是相當重要而且關鍵的觀點。

但無疑的,隨著電腦科學及軟體工程的演進及發展,在大多程式員日常的開發生活中,所需的絕大多數演算法和資料結構,早已一應俱全。不僅教科書上都有完整的介紹,而且幾乎都以包裝成為高階的程式庫,程式員可以看待一個一個黑盒子的方式來運用它們。對於許多程式員來說,如何更有效、有更彈性的組裝它們、運用它們,成了更為要緊的議題。

此外,有些重視程式設計演算觀點的程式員,他們只在意程式執行的效率或演算方法的良窳,但卻不甚關心他們所寫下的程式碼是否容易閱讀、是否容易擴充、是否容易維護、是否容易修改、是否足夠模組化以便輕易的與其他模組搭配一同使用。這樣的論點並不想一竿子打翻一船人,在我的經驗中,能夠撰寫出既高效、架構又優雅的程式員同樣也是所在多有。但是不可諱言的是,不太在意程式架構只追求效率的程式員在我們的身邊也是時有所見。

有一個普遍的成見是,能撰寫高效演算法程式碼的程式員,才是真正具備價值的程式員。而這樣的看法,無疑的抹滅掉軟體架構的重要性,畢竟,對軟體開發而言,效能固然重要,但也不是那唯一的聖杯。大多數軟體產品或服務的開發,所遭遇到最大瓶頸往往不在效能,而在開發的生產力,生產力則深受軟體架構的優劣所影響。正如前文所提及的,日常所需的演算法,早已被研究的相當完備,而且各式程式庫垂手可得,效能的最佳化,最好的方式便是交付給各領域的專家們。一般應用程式開發者的責任,在這個時代中已經有所不同。

從上述論點來看,讀者或許會猜想,我認為從演算觀點出發的程式設計在這個時代中已經不重要了。其實不然。

我們固然可以看到許多不重視程式架構的程式員,但我們同樣也可以看到許多不重視演算的開發者。高階包裝的抽象化程式庫固然將程式員隔離於許多演算法及資料結構的細節之外,使得他們得以輕易的加以運用於自己所開發的系統中。但是,正因為接觸不到細節,也有可能使得他們忽略這些演算法的特性,尤其是效能的特性。

為什麼即使已經有許多現成的演算法及資料結構,甚至是其他用途的程式庫,程式員還是必須修習像演算法及資料結構這樣子的課程?

演算法基本是一門探討以電腦程式解決問題的學問,時間複雜度和空間複雜度是貫穿演算法這門學問的主要支柱。時間複雜度和空間複雜度所代表的是什麼?正是代表解決問題在時間上以及空間上所需付出的代價。我們學習演算法,除了學習常見的演算問題分類,以及如何解決這些演算問題之外,更重要的是要學習解決問題的方法,以及培養解決問題時的成本意識。

即使教科書上列舉了許許多多的問題以及解決它們的演算法,但無論列舉再多,都無法窮舉完所有你在現實的開發生活中所會遭遇到的所有問題。或許你可以憑藉著組合既有的演算法來解決問題,但你或許還需要發展新的演算法才能夠因應。這便是我所謂學習解決問題的方法,即使問題沒有出現在教科書中,你還是要有能力找出一個可接受的解決方法-即使不是最佳的方法。

許多應用程式的開發者,其實並不太會遭遇到需要發展複雜或困難演算法的情況。但對演算法的基礎了解,可以、也應該培養我們對於運算成本的概念。現今程式庫高度發展的現況,似乎讓程式員們有了忽略運算成本的趨向。就以Java的標準程式庫當做例子來說吧,java.util.ArrayList這個容器類別有個叫做indexOf()的函式,傳入某個物件,可以找到該物件在ArrayList中的位置索引值。這個動作,其時間複雜度其實是線性的,和ArrayList中所含有的元素總數成正比。可是,程式員可能不會意識到其成本。也許程式員在撰寫程式的過程中,需要記錄不重覆的元素,他可能利用ArrayList來記錄,並且利用indexOf()來判斷重覆與否。但是,indexOf()其實是一個代價不低的動作,同樣的需求,利用java.util.HashSet這個類別的contains()函式,其時間複雜度是常數!這二者都能達成相同的需求,但是執行效率隨著規模的提昇,卻有著天壤之別。

這只是個簡單的例子,但我想表達的是,程式設計朝向抽象化是正確的方向,但高度的抽象化或許或少造成程式員失去對於許多運算之特性的了解,尤其是它們的演算特性。就如同上例所表達的,抽象上來看,兩種寫法在邏輯上都正確,也都能得到正確的結果,但因為有著不同的演算特性,使得它們最終的效能表現大大不同。

即使大多數的應用程式開發者毋需憂心於新型演算法的設計問題,但是對於常見的演算法諸如排序、搜尋等,最好具備一定的認識,在撰寫程式時才能知道究竟有那些工具可供取用。而且,更重要的是,在撰寫程式碼時,隨時都應該對演算的成本保持警覺,尤其對於每一個高階的抽象操作,都應該確切的了解其演算的成本及代價。高階抽象包裝有時就像糖衣,它包覆的究竟對你來說是不是毒藥,必須要時時刻刻小心。

倘若在撰寫程式時,僅考慮架構上的特性,缺乏成本意識、忽略了演算特性,那麼還是很容易在無意中寫下效能低落的程式碼,無法設計出新的演算法不是真正的問題所在,這才是真正的問題所在!

「程式設計是一種多面向特質的活動」。單從任何一個面向來檢視程式設計的特質,都難免有所偏頗及不足處。但不論是演算觀點下的程式設計,或架構觀點下的程式設計,都捕捉了程式設計這項活動中部份的迷人之處。也只有從多方觀點齊下,才能讓自己的程式碼無論從什麼角度來檢視,都能呈現出美的一面。

原文出處:http://www.javaworld.com.tw/roller/qing/entry/%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88%E7%9A%84%E5%85%A9%E5%80%8B%E8%A7%80%E9%BB%9E_1_2

評論(0)

發表評論