Saturday, November 12, 2011

程式設計的兩個觀點 (2/2)

程式設計的兩個觀點 (2/2)
程式設計是一種多面向特質的活動,既可以從演算的觀點來看待,也可以從架構的角度來加以檢視。從演算的觀點來看,程式設計的焦點在於如何讓計算機器以有高執行效率、節省資源的方式來解決特定的問題。而架構觀點則重視如何以有彈性、容易擴充、容易改變的方式,迅速的組裝搭配各種建構區塊,並從中獲取最大的生產力。
這兩種角度都反映出某種程度程式設計活動的本質,但卻又都不完全。所以我們時常會看到一個現象,有些程式員信仰程式設計的演算觀點,往往認為演算法才是程式設計的王道。對他們來說,程式的時間複雜度,或具體的執行效率是凌駕於一切的指標。程式的美感便於此處展現。信仰架構觀點的程式員,重視則是軟體架構上的特性,例如可擴充性、彈性、生產力、易於理解、等等。對他們來說,程式的美感,有著不同的定義方式。
通常在校園中,電腦科學的程式設計課程多半偏重演算觀點的角度。但是,一旦進入職場後,似乎軟體架構的重要性又在這演算能力之上。當然,這並無法以偏概全,有些人的程式設計工作,便是在發展新的演算法,例如從事電腦輔助IC設計系統的人。又例如像開發多媒體編解碼器的程式員,其工作對於效能輜銖必較,非得千方百計的壓榨電腦的每一分力氣。對這類程式員來說,演算觀點自然是相當重要而且關鍵的觀點。
但無疑的,隨著電腦科學及軟體工程的演進及發展,在大多程式員日常的開發生活中,所需的絕大多數演算法和資料結構,早已一應俱全。不僅教科書上都有完整的介紹,而且幾乎都以包裝成為高階的程式庫,程式員可以看待一個一個黑盒子的方式來運用它們。對於許多程式員來說,如何更有效、有更彈性的組裝它們、運用它們,成了更為要緊的議題。
此外,有些重視程式設計演算觀點的程式員,他們只在意程式執行的效率或演算方法的良窳,但卻不甚關心他們所寫下的程式碼是否容易閱讀、是否容易擴充、是否容易維護、是否容易修改、是否足夠模組化以便輕易的與其他模組搭配一同使用。這樣的論點並不想一竿子打翻一船人,在我的經驗中,能夠撰寫出既高效、架構又優雅的程式員同樣也是所在多有。但是不可諱言的是,不太在意程式架構只追求效率的程式員在我們的身邊也是時有所見。
有一個普遍的成見是,能撰寫高效演算法程式碼的程式員,才是真正具備價值的程式員。而這樣的看法,無疑的抹滅掉軟體架構的重要性,畢竟,對軟體開發而言,效能固然重要,但也不是那唯一的聖杯。大多數軟體產品或服務的開發,所遭遇到最大瓶頸往往不在效能,而在開發的生產力,生產力則深受軟體架構的優劣所影響。正如前文所提及的,日常所需的演算法,早已被研究的相當完備,而且各式程式庫垂手可得,效能的最佳化,最好的方式便是交付給各領域的專家們。一般應用程式開發者的責任,在這個時代中已經有所不同。
從上述論點來看,讀者或許會猜想,我認為從演算觀點出發的程式設計在這個時代中已經不重要了。其實不然。
我們固然可以看到許多不重視程式架構的程式員,但我們同樣也可以看到許多不重視演算的開發者。高階包裝的抽象化程式庫固然將程式員隔離於許多演算法及資料結構的細節之外,使得他們得以輕易的加以運用於自己所開發的系統中。但是,正因為接觸不到細節,也有可能使得他們忽略這些演算法的特性,尤其是效能的特性。
為什麼即使已經有許多現成的演算法及資料結構,甚至是其他用途的程式庫,程式員還是必須修習像演算法及資料結構這樣子的課程?
演算法基本是一門探討以電腦程式解決問題的學問,時間複雜度和空間複雜度是貫穿演算法這門學問的主要支柱。時間複雜度和空間複雜度所代表的是什麼?正是代表解決問題在時間上以及空間上所需付出的代價。我們學習演算法,除了學習常見的演算問題分類,以及如何解決這些演算問題之外,更重要的是要學習解決問題的方法,以及培養解決問題時的成本意識。
即使教科書上列舉了許許多多的問題以及解決它們的演算法,但無論列舉再多,都無法窮舉完所有你在現實的開發生活中所會遭遇到的所有問題。或許你可以憑藉著組合既有的演算法來解決問題,但你或許還需要發展新的演算法才能夠因應。這便是我所謂學習解決問題的方法,即使問題沒有出現在教科書中,你還是要有能力找出一個可接受的解決方法-即使不是最佳的方法。
許多應用程式的開發者,其實並不太會遭遇到需要發展複雜或困難演算法的情況。但對演算法的基礎了解,可以、也應該培養我們對於運算成本的概念。現今程式庫高度發展的現況,似乎讓程式員們有了忽略運算成本的趨向。就以Java的標準程式庫當做例子來說吧,java.util.ArrayList這個容器類別有個叫做indexOf()的函式,傳入某個物件,可以找到該物件在ArrayList中的位置索引值。這個動作,其時間複雜度其實是線性的,和ArrayList中所含有的元素總數成正比。可是,程式員可能不會意識到其成本。也許程式員在撰寫程式的過程中,需要記錄不重覆的元素,他可能利用ArrayList來記錄,並且利用indexOf()來判斷重覆與否。但是,indexOf()其實是一個代價不低的動作,同樣的需求,利用java.util.HashSet這個類別的contains()函式,其時間複雜度是常數!這二者都能達成相同的需求,但是執行效率隨著規模的提昇,卻有著天壤之別。
這只是個簡單的例子,但我想表達的是,程式設計朝向抽象化是正確的方向,但高度的抽象化或許或少造成程式員失去對於許多運算之特性的了解,尤其是它們的演算特性。就如同上例所表達的,抽象上來看,兩種寫法在邏輯上都正確,也都能得到正確的結果,但因為有著不同的演算特性,使得它們最終的效能表現大大不同。
即使大多數的應用程式開發者毋需憂心於新型演算法的設計問題,但是對於常見的演算法諸如排序、搜尋等,最好具備一定的認識,在撰寫程式時才能知道究竟有那些工具可供取用。而且,更重要的是,在撰寫程式碼時,隨時都應該對演算的成本保持警覺,尤其對於每一個高階的抽象操作,都應該確切的了解其演算的成本及代價。高階抽象包裝有時就像糖衣,它包覆的究竟對你來說是不是毒藥,必須要時時刻刻小心。
倘若在撰寫程式時,僅考慮架構上的特性,缺乏成本意識、忽略了演算特性,那麼還是很容易在無意中寫下效能低落的程式碼,無法設計出新的演算法不是真正的問題所在,這才是真正的問題所在!
「程式設計是一種多面向特質的活動」。單從任何一個面向來檢視程式設計的特質,都難免有所偏頗及不足處。但不論是演算觀點下的程式設計,或架構觀點下的程式設計,都捕捉了程式設計這項活動中部份的迷人之處。也只有從多方觀點齊下,才能讓自己的程式碼無論從什麼角度來檢視,都能呈現出美的一面。
Reference:

No comments: