在現在數位相機當道的年代,不知道還有多少人曾經看過以前某個底片廣告,還記得那句經典台詞「拍誰像誰,誰拍誰誰都得像誰」,對於程式設計來說,從第二種語言的使用開始,也經常會面對類似的問題,對認真負責的開發者來說,總會想寫出符合該語言原汁原味的程式,避免明明用A語言寫卻看起來像是B語言的調調。
Java寫的像C?
在過去Java如日中天的年代,印象中最常看到的類似抱怨是「把Java寫得像C」,這特別是指撰寫Java時,是否善用了Java物件導向風格,還是把一個又一個的靜態方法當作函式來使用,早期的文件甚至書籍甚至也會有這種情況,因為Java是C-like的語言,語法與關鍵字有許多重疊之處,許多開發者或作者就只是將一些關鍵字換一下,就開始將C一對一地「翻譯」為Java程式。
語法與關鍵字相近的語言,似乎特別容易有這類問題,記得朋友曾經說過:「在一個C#的專案中,雖然你不知道程式碼經手過哪些人維護,但是經常很容易就能看出,哪個部份是曾經熟悉Java的開發者寫的,哪個部份是一開始就使用C#的人寫的。」在JavaScript剛開始鹹魚翻身的年代,也有許多人試著將之與Java相比,甚至特意寫的像Java,忽略了JavaScript本身具有一級函式、基於原型(prototype-based)的特性等。
或許是不少開發者看過多次類似的情況了,在打算使用另一門語言開發程式時,開始會重視起語言的原汁原味這回事,像是最近收到的一個課程邀請中,就有著「請老師增加C++跟Python寫作風格差異跟一些慣例,避免我們寫Python看起來像寫C++」這樣的需求。由於看太多「Java寫的像C」的案例,在我學習其他程式語言的時候,也時時在留意著該語言的風格典範、語言慣例、語法特性等,而最近在撰寫Python程式語言紙本書時,也更時時考量著,寫出來的範例是不是夠Pythonic!
為什麼要追求Pythonic?
由於Python是個風格鮮明,且特別強調Pythonic,也是目前我僅見特別有此一形容詞的語言,想要知道「為什麼寫A不能像B?」、「為什麼要追求語言的原汁原味?」等的答案,搜尋一下Pythonic這個關鍵字,其實就可以得到不少的闡述與討論,將Pythonic這形容詞拿掉,多半也就是其他語言也能適用的答案。
一門語言從出現到成長到累積足夠的使用者,這中間會累積越來越多的使用經驗,也就會有許多關於語言如何正確使用,如何讓語言能具有可讀性、易維護、正確且執行效率高的慣用法,在一門語言演進而打算加入新特性時,經常也將這些慣用法納入考量,甫出爐不久的ECMAScript 6就是個具體實例,當中不少新語法都是基於既有的慣用法而來。
慣用法的累積與語言的納入演化是交相迭代的,因此就算語言彼此之間語法的再相近,慣用法往往都無法直接從另一門語言移植過來,若真有人這麼做,那部份的程式碼跟既有的專案就會出現格格不入的感覺,可能看來不易閱讀,也許多餘而冗長,甚至影響了元件間彼此的整合,或者應用程式整體的執行效率。
不過,為了追求語言的慣用法,一開始就將兩門語言之間的語法、排版風格等逐條列出,對我來說其實是件極其無趣且沒意義之事,某些程度上我甚至覺得,這就跟逐條列出Python 2與Python 3之間的差異性,一樣的無聊。瞭解一門語言的慣用法對我來說,從來就不是這樣透過逐條比較而來。
吸收語言的慣用法
語言的慣用法,大致上可以分為兩個面向來看待,一是命名與排版,二是語言特性。對於前者,其實現代程式語言,多半會有份官方文件,整理了語言慣用的命名方式,或者是排版風格。像是Python的PEP 8就是個例子,若官方沒有列出,社群間多半也會有類似文件,甚至不同公司或團隊也會有自己的一套規範,像Google就會有自己的〈Google Java Style〉,或〈Google Python Style Guide〉。
這說明了,除了考量語言本身的命名方式或排版風格之外,若團隊開發時有需要,也可以基於既有的命名與排版慣用方式,制定一套適用的規範。此時,若有工具輔助更好,就像Python的強制縮排是一個例子;Go語言認為這個問題太複雜,本身就提供gofmt來協助格式化是個很棒的主意──不少整合開發工具在編輯器上,都有可自訂的排版功能,而像JavaScipt也有JSLint這類工具,可以善加利用。
是否善用語言特性這點就比較麻煩,就結論來說,方式之一就是透過大量原始碼的閱讀來培養。那麼到哪去閱讀原始碼?官方的標準程式庫原始碼是絕佳的好地方,舉例來說,在實作Python物件的__eq__()方法時,我曾經試著遵循著Java的實作方式,在Python中使用isinstance()來判斷物件型態,然而在閱讀標準程式碼原始碼中就發現,多數情況下並不使用isinstance(),而只是使用hasattr()來判斷物件是否具有相關屬性,我才想起Python是門動態語言,而動態語言多數情況下只需考慮行為而非型態的慣例。
另一個吸收慣用法的來源,就是API官方說明文件,像是Python的標準程式庫說明文件就提供了大量範例,如果願意細心閱讀,可以發現不少的語言的特性與技巧。
例如,Python建議的檔案讀取風格:讀取一個檔案最好的方式,就是不要去read!因而常見的慣用法就是使用for in來迭代檔案物件,然而,若想讀取文字檔案內容,並將每一行安插入set。官方文件中就談到的一個簡潔方式,是將檔案物件直接傳給set:
unrepeated_lines = None with open('filename', 'r') as f: unrepeated_line = set(f)
想要善用語言特性的方式之二,其實與是否願意持續重構程式碼有關,就這點來說,一名開發者可以一直將Java寫的像C,絲毫沒有用到任何物件導向的特性,若不是他撰寫的程式碼,完全嗅不出任何的餿味(Bad smell),或者是程式中的資料,從來就不曾與某組操作發生過密切關聯的話,那多半就表示這名開發者不在意也不會去檢視程式碼的品質,更別說運用重構手法來善用語言特性,以便改進程式可讀性、維護性,甚至是效率了。
是否願意持續改進程式碼?
如果已經熟悉一門語言,在試著運用另一門語言的同時,運用已熟悉語言的經驗,其實是件很正常的事情,這讓過去我們一些有用的作法得以延續,以便保證目前撰寫的程式能夠運作,此時,寫A卻像B並不是太大的問題,當然,若想在程式開發的一開始,不讓寫A卻像B的情況太嚴重,那麼整個團隊成員對語言有一定程度的瞭解,事先適當地制定規範會是必要的。
在這樣的前提下,剩下的就是團隊或成員,對於程式碼品質是否在意?是否願意持續重構程式碼有關了!隨著對一門語言運用的經驗越多,看到的文件與原始碼越多,必然會發現更多適合的作法,可以改進過去撰寫過的程式碼,若團隊或成員能夠有意願與時間(或者被允許)進行重構,那麼會比一開始,就試圖條列兩門語言的寫作風格差異跟一些慣例,來得有意義且更能擁有具體成效!