追求神乎其技的程式設計之道(十一)- 抽象化與命名

追求神乎其技的程式設計之道系列:

休眠已久的神乎其技系列又復活了!這篇文章其實寫很久了,只是一直斷斷續續到今天才完成它,久到讓很多人覺得這系列已經完結了…。但我想只要我還有在寫程式,這系列就永遠不會結束吧。

簡潔、彈性、效率

我一直覺得寫程式是一種藝術活動。程式語言是一種要求極度精確的表達方法,只要少打一個字母就可能造成完全不同的結果,但同時卻又不限制你要如何達到目標。

程式設計師有極大的自由來讓一個程式按照自己的想法「活起來」,不同人針對同樣的目標所寫的程式也一定不同。有人會用極簡主義來把變數命名為a、b、c,也有人會把用匈牙利命名法讓變數前後長出鬍子和尾巴;有人堅守DRY原則(Don’t repeat yourself),只要類似的程式出現兩次,就把他們抽象化成一個函數,也有人用copy/paste寫程式,不管怎麼page up或page down都一直看到一樣的東西還能泰然自若;有人寫程式把所有東西都塞在main裡面,也有人寫個Hello world就要搞一個class HelloWorld(雖然有些時候是被囉嗦的J語言強迫的…);有人沒聽過Big O也寫程式寫得很開心,但也有人嫌stdlib的qsort太慢硬是要自己重寫一個…。

儘管每個人的信仰和原則不同,但大體上程式藝術家也不過是在「簡潔」、「彈性」、「效率」這三大目標上進行一連串的取捨(trade-off)和最佳化。

「簡潔」的程式也「易讀」,沒有多餘的敘述或重複的程式碼,每個概念都只有唯一的一段碼在描述它。如果多了,就容易產生不一致的行為,如果少了,就是沒做到該做的事。有「彈性」的程式容易修改和擴充,只要在一個對的地方彈彈手指,不用因為老闆朝三暮四或是需求改變就得把整個程式重新翻修一次。有「效率」的程式會用最適合的資料結構存放每一樣資料,用最快的演算法做每一項必要的計算,並去除任何不必要的間接行為 (indirection)。

雖然目標很明確,但程式設計之所以像藝術就是因為大部分時候我們都沒辦法兼顧這三項目標:為了效率,可能就得犧牲彈性和簡潔;反過來說,為了彈性或簡潔,也常得犧牲效率作為代價。幸運的是,效率的追求在電腦硬體和編譯器技術的進步下已經不像20年前那麼重要,只要選對資料結構和演算法,幾乎已經沒有必要手動做低階的最佳化。除去效率之外,彈性和簡潔其實是比較容易同時達到而又不互相衝突的目標。要達到這目標,其中關鍵的能力就是今天的主題:「抽象化」(abstraction)。

最簡單但也是最難的事情

很多人沒聽過抽象化這個詞,甚至以為自己不會這件事,但其實從我們宣告第一個變數起,抽象化就已經開始了。

「這個變數要叫什麼名字?」

幫變數命名時,其實就是在賦予那個變數一個「意義」。人的記憶力有限,很難記住大量且沒有意義的資訊。但如果資訊有了一個固定且有邏輯的名字,我們也就有一個容易記憶的符號來代替整個複雜的概念。換句話說,我們可以把非常複雜的概念濃縮為一個容易處理和記憶的小單位,這個過程就叫做「抽象化」。

抽象化可以讓程式變得簡潔。好的程式設計師會習慣從重複的程式碼中找出共同或相似的部份,並且把這個部分提取出來變成一個更通用的概念。任何複雜的概念都可以被抽取出來替換成一個變數、一個函數、一個類別、一個模式、一個模組、甚至是一個系統,並加上適當的命名,就能讓這個程式「一看就懂」,任何註解都不需要寫。抽象化也能讓程式有彈性。經過適當抽象化的程式,每個概念都有一個獨立的「單位」(可能是變數、函數、類別、模組、或系統)可以表示,每個概念中包含的細節也被隱藏在適當的範圍內,不管要修改或擴充原本的程式都能讓需要碰觸的地方減到最少。

雖然抽象化是讓程式簡潔又有彈性的關鍵,但出乎意料的這是一個容易理解卻很難精通的能力。抽象化做得太少,程式會變得凌亂不堪,不同層級的概念和資訊互相交雜在一起,不僅讓程式變得難讀也難改。抽象化做得太多,就是所謂的over design,明明需求只有印一個Hello World,卻用了10種design patterns蓋起101大樓以應付根本就不會出現的「未來需求」。

抽象化這個主題可以講三天三夜講不完,但今天我只想提其中最簡單也最難的事:「命名」。

命名可以說是寫程式時最簡單但也是最難的事了。這件事沒什麼人會教,沒多少書會寫,因為這件事看起來非常容易,即使你把程式裡的變數照字母順序a, b, c, d, e, …命名也是行得通,反正對編譯器來說變數或函數的名字不過就是一個沒有意義的符號,不管你取什麼名字最終都只是對應到一個像是0x08048374這個樣子的記憶體位置而已。

簡單來說,一個變數是叫「小狗」或是「小貓」,對電腦來說都沒有區別,但對人來說,差別可大了。

很多初學者以為程式是寫給電腦看的,只要看起來好像能跑出正確結果就好,所以變數位置隨便亂放、名字也隨便亂取、每個變數都是public、甚至一個函數有幾百行,為了在一個畫面中塞下更多程式碼還把IDE的字型縮小到要瞇著眼才看得見。也有很多人覺得高手寫的程式看不懂是正常的,等到自己等級提昇後應該就會看得懂了,但其實事實完全不是這樣。我認識的每個高手和大師寫的程式碼都是乾淨、簡單、易懂,即使是極端複雜的演算法,都能直接從程式碼中看懂作者的想法。

Martin Fowler的 “Refactoring – Improving The Design of Existing Code” 一書中有一句話我很喜歡。

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. (任何一個傻瓜都能寫出計算機可以理解的程式碼。唯有寫出人類容易理解的程式碼, 才是優秀的程式員。)

一段好的程式碼是不需要任何額外註解或說明的。如果名字都取得好,每個變數就能適當的解釋了自己的角色,每個函式都說明了自己的功能,整個程式讀起來就會像在讀說明文件一樣自然。在這種境界下,只要有了基本背景知識的程式員應該都要能輕易地看懂。英文中有個詞叫做explain itself很適合用在這,也就是自己應該要能完美的解釋自己的一切,不需要其他的人或文件來幫忙。

但是,命名是很難的一件事,可以說是寫程式中最接近「藝術」的一部分了。我說的命名,不是要用大小寫混雜的「CamelCase」或是底線分隔的「underscore_separated_style」這種風格問題,而是一個方形到底要叫rectangle或是x的差別。名字取得好,不但自己或其他人未來再回來看這份程式碼時容易進入狀況,對於正在開發中的程式也可以減少很多不必要的bug。

我之前當一門課的助教時,有個作業是要學生實作一個西洋棋遊戲,畫面上要有個棋盤,還有該有的棋子。既然是個棋盤,底層很自然的就會用個二維陣列來表示棋盤的狀態,例如說我們會有

Chess board[N][N]

這樣子的一個陣列。接下來,真正的問題來了,程式中勢必會有一些兩層的for迴圈去對這個陣列做操作,如果是你會把這兩個迴圈的index變數取做什麼名字?

最常見也最不用腦的index命名就是i和j,在一般沒有特殊意義的迴圈中用i是沒什麼太大問題的,因為大家都知道這只是一個單純的index。但如果用到j,通常就代表程式可能有些臭味了,至於會用到k、l、m… 那這個程式一定是徹底腐敗了。

我看了很多學生的程式,我發現很多有bug的程式都是用i、j,或是x、y來命名,而那些寫得很漂亮的程式,幾乎都是用row和column來命名(或是他們的縮寫r和c,或是row和col)。

用i、j的問題在哪?

問題在這兩個名字沒有和棋盤的位置有直接關連,看程式的人沒辦法一眼看出你的i到底是指row還是指column,或是指到宇宙裡的一顆星星。即使是正在寫程式的作者本人,也得一直在心中做i是row、j是column的轉換,但只要精神稍不集中,或是吃個飯休息回來,很輕易就會忘記這些隱晦(implicit)的對應關係。而這種隱晦的對應,就是傷害程式碼可讀性和造成bug的通緝要犯之一。有的人為了避免自己忘記這些細節,就會把這種隱晦的關係或假設寫在程式的註解裡。但話說回來,既然要寫,直接寫在程式碼裡不是更好嗎?

除了用i、j的這群人外,還有另外一群用x、y的程式也是讓人非常頭痛,如果要我比較的話,我會說用xy比用ij還糟糕。為什麼?因為這個程式最終要把棋盤畫在螢幕上,而所有2D繪圖的函式庫都是用x、y來表示螢幕上的位置,如果棋盤用xy,螢幕繪圖也用xy,這樣如何分辨這個xy是棋盤的位置還是螢幕的位置?用xy這群人的解決方法都大同小異,比較懶惰的就是用x1、x2,甚至是x和xx;好一點的會用boardx和screenx,但以index變數來說還是太長太囉嗦了。

與其費這麼大力氣區分兩種xy,如果一開始就用完全不同的名字來存取棋盤和螢幕,不就沒事了?以二維陣列來說,用row和column符合natural mapping,不用再心中自己多做一次轉換。此外,現代程式語言的多維陣列大多是row-major排列,也就是說A[r]就能取到第r個row,A[r][c]就能取到第r個row的第c個元素;但如果用xy來存取二維陣列,就要把xy反過來,寫成A[y][x]才能取到第y個row的第x個column。
(在這個程式中很多用xy的人都把row和column順序搞反,導致初始化的盤面整個轉了90度。)

我以前參加程式比賽時,看過很多經過長期訓練的選手因為比賽的時間壓力而養成不好的習慣,像是把所有程式碼寫在main裡面,變數不是aa就是bb這種沒意義的名字。在程式比賽這種特殊的環境裡,每個程式的目的就是解一個有明確輸出入規定的問題,加上有時間限制,所以選手們都是盡量用最短的code來實作自己的想法。這種情況下寫的程式可以說是用完就丟,只要比賽一結束這個程式的生命也就到了盡頭,所以很多人就不會去思考命名的問題。

到大學的時候,我也常幫同學在作業deadline前夕看他們的程式幫忙debug。很奇妙的是,大學課程的期末專題或是作業應該都有充裕的時間可以慢慢「設計」一個程式,但很多人都是在最後一兩天才開始動手,於是在作業死線的壓力下也沒心情去好好設計一個程式的架構,更別提要好好想每一個變數的命名和位置,也就浪費了許多可以好好練習這個命名藝術的機會。

命名和抽象化是一體兩面的事情。當你能把一個概念用一個適當的名稱來稱呼它時,你才有辦法把這個概念當成一個基石往上建構更複雜的事物。在此同時,人們也才能用這些簡單的名稱來討論複雜的概念或想法。如果你在寫程式時常常沒辦法用很簡單的話跟別人解釋你的程式,通常也代表你的程式是一團漿糊,沒有條理和層次。在這種情況下,你怎麼知道漿糊裡是不是黏了一堆臭蟲呢?反過來說,當你能用簡單清晰的白話跟人解釋你的程式時,你也一定能把程式寫得一樣乾淨漂亮有條理。

如果你現在還在用a, b, c這種變數寫程式,不妨先暫停一下,好好想想每個變數的意義是什麼,你的程式就會自然的變得越來越簡潔和漂亮。

(待續)

2/1 更新:
有朋友提到一篇有趣的相關文章:軟體業的重要職缺 命理大師!。這文章說軟體公司應該有個專門掌管命名的人,才能保持整個project的一致性,並順便算個命看看這些名字吉不吉利。

這讓我想到,其實現有open source程式這麼多,我們可以很容易的寫一個「命理大師」程式出來。只要到幾個project host site,像github、google code之類的地方,把所有project裡的程式碼token抓出來做一些簡單的分析和統計,就可以得到一些有趣的資訊和命名時的參考。例如說,我們可以知道有多少程式裡面用Box表示方形,多少程式用Rectangle,多少程式用deleteXXX,多少用removeXXX,他們之間的區別又在哪。甚至在設計library或API時,連function參數的多寡和排列順序,都可以從此得到參考資訊。更進一步,可以用word net把這些token做clustering,之後我們就可以打一些關鍵字,甚至打中文,讓這個程式建議最多人用的習慣命名法…。

15 thoughts on “追求神乎其技的程式設計之道(十一)- 抽象化與命名

  1. 關於 “很多初學者以為程式是寫給電腦看的,只要看起來好像能跑出正確結果就好”:包括我自己在內的另外一種人,則是常會過份在意所謂命名的藝術。建議大家要在走得太慢時思考自己是被什麼絆住了,跑得快時思考什麼地方可以多停留一會兒:初學者寫程式要記得程式的大架構跟流程比函式、變數命名來得重要;進階者要注意與人合作時要定義共通的 coding style、回頭省視自己在命名上是否簡潔有力。

    • 追加想法:可以說跟我有類似苦惱 (煩惱架構 / 命名問題而佇足不前) 的初學者,在苦思程式的大架構之前,得先把問題「具象化」。這或許是部份初學者首先得鍛鍊到的技術。:P

      • 定架構、抽象化、命名,這其實都是同一件事情,只是著眼的距離不同。定架構是從上往下,抓出解決問題的關鍵大元件,在幫這些元件命名的同時也就定義了他們的角色和彼此之間可能的關係。越往下走,要命名的角色單位就越來越小,但同樣的還是得靠名字來定義他們的用途和關係。聽起來簡單,但難就在難要同時見樹又見林…。

        另外,把問題「具象化」的確非常重要。解決問題前不先定義好問題,很容易就會做上一堆白工。

  2. Pingback: Tweets that mention 追求神乎其技的程式設計之道(十一)- 抽象化與命名 | vgod’s blog -- Topsy.com

  3. vgod你好:

    小弟也是個很喜歡寫程式的人,從你的第一篇慢慢看到現在,可以想像你為啥喜歡寫程式,果真還是因為你的熱情。而第一篇那個Foxman的回覆也讓我感觸很深,我也是寫了十年的人,而這幾年我也開始思考我究竟為何而寫。寫軟體這十年來從不會寫到會寫,從執著工具,技術再到什麼都不執著…再到使用軟體的人需要什麼,直到台灣的軟體產業…一連串下來,我想也還是如同這位Foxman所講的:「沒有所謂的技巧,只有解決實際的問題」。但說真的,我也很想反問:「這樣子寫程式快樂嗎?」你現在在MIT也能夠還是用你的熱情在面對這些程式,研究的問題嗎?而我也好奇五年後如果這個blog還在的話,你將來也依然還是會如此想嗎?

    此外你以前想要用程式寫程式,這個我與我的程式設計師爺爺都想過,表示這個問題很多古人都想過。但到我這一代我忽然發現,程式設計不僅是一門科學,也是一門藝術。小程式都無所謂,但程式一大,便會開始出現anti-pattern或是model的解法,有時候甚至會用些爛方法,但其實只是為了開發進度及實做程式設計師能力的平衡。這讓我深深覺得能達到以圖形方式規劃程式,然後再產生程式碼架構或一些主迴圈,已經是完成一個很大的挑戰了。如果某一天演算法的目的不是求解,而是有目標地創造多樣性(聽起來也根基因演算法前半段很像),等到哪一天程式可以藉由sensor及網路收集情報,然後如同人類一樣在畫畫,那或許到時候那個程式也就可以寫程式了。

    • 程式的目的當然是用來解決問題的,但寫程式的過程可以是很有趣和快樂的。
      我即使到現在還是都很快樂的在寫程式,常常沉迷於coding而把我真正的研究工作放一旁。因為寫程式很有趣,所以很多小事情即使乍看之下寫程式去做會比手動去做還花更多時間,我還是會選擇讓電腦去做它能做的所有事情。長久下來,我寫程式已經變成不是只為了工作或研究而寫,而是為了讓自己能更輕鬆更省力氣的工作和生活。當自己寫的程式能搔到自己的癢處時,自然就會有熱情和動力持續下去。

      這個blog已經9年囉,如果認識我更久的就知道我在某個bbs上還有個人板,可以追朔到我高中時期的文章。我的熱情已經燒了十幾年,我想之後還是會繼續下去吧。

  4. 你好我也是一個資工的學生
    最近正在煩惱
    我因為程式不好
    而要不要繼續念資工
    看了這麼多篇的文章
    才深知 實力是要培養訓練出來的
    我還是需要多加練習才可以
    不然永遠都不會變強

  5. 很同意你的变量“自注释”的观点,但是我认为他不一定永远适用,还是有自注释会让程序名变的长长的然后可读性下降的时候,这时可以选择取一个短一点的变量名然后变量名前一行或者后面加一行注释,这也是一种 trade-off

    • 如果變數名被迫要取得很長,通常意味著抽象化的程度不夠。一種可能是它做了兩件以上的事,那就應該要把它橫向切開分成多個變數,或是縱向往上抽出class或function。另一種可能是它被放在不對的地方,以致於沒辦法用class或function名來加以解釋。
      如果在合適的context之下,我相信變數名通常不需要取很長的。如果您能提供個範例讓大家知道什麼情況一定得取很長的變數名,那一定會有很大的幫助。

  6. 我的感覺 「簡潔」「彈性」 並沒有直接的衝突
    然後 「妥協」的因素反而常見。所以分成
    (「簡潔」「彈性」)、「效率」、「妥協」 這三大目標上進行一連串的取捨(trade-off)

  7. 我也时常因为变数的命名而头疼,我的概念也和你的一样,希望在程式里面就能够理解程式是要来做什么。我以前觉得写得别人看不懂的人才是高人,一直都认为自己写的简单,别人一看就懂的不算高手。

    我想除了变数外,程式的结构也是很关键的,有时候开始想的一个结构可以完成任务,可当写下来时候,遇到许多没有想到的事情,可能要转几个湾才能够达到效果,所以我认为程式的结构也对于写好简洁程式是起到很大作用的,我自己的是先把要完成的任务分解多个模块,先编写模块的结构,再细写模块里面的程式,这个能够用于多人协作,不会至于乱套。

    图形化编程我觉得是提高程式可读性的唯一办法,就像以前的DOS到windows的转变一样,我在工业控制编程方面已经有出现,已经有一套的国际的标准,可以到你的博客追求神乎其技的程式設計之道系列2,留言

  8. 最後一段說的我記得在Knuth教授的個人網頁上看過類似的東西, 有興趣可以研究一下

  9. Pingback: My Love, My Live » [碩士] 寫程式是一件很快樂的事。

留言給我吧!