Reload Original PagePrint PageEmail Page

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

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

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

簡潔、彈性、效率

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

程式設計師有極大的自由來讓一個程式按照自己的想法「活起來」,不同人針對同樣的目標所寫的程式也一定不同。有人會用極簡主義來把變數命名為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, …命名也是行得通,反正對編譯器來說變數或函數的名字不過就是一個沒有意義的符號,不管你取什麼名字最終都只是對應到一個像是0×08048374這個樣子的記憶體位置而已。

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

很多初學者以為程式是寫給電腦看的,只要看起來好像能跑出正確結果就好,所以變數位置隨便亂放、名字也隨便亂取、每個變數都是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,之後我們就可以打一些關鍵字,甚至打中文,讓這個程式建議最多人用的習慣命名法…。


免责声明:
当前网页内容, 由 大妈 ZoomQuiet 使用工具: ScrapBook :: Firefox Extension 人工从互联网中收集并分享;
内容版权归原作者所有;
本人对内容的有效性/合法性不承担任何强制性责任.
若有不妥, 欢迎评注提醒:

或是邮件反馈可也:
askdama[AT]googlegroups.com


点击注册~> 获得 100$ 体验券: DigitalOcean Referral Badge

订阅 substack 体验古早写作:


关注公众号, 持续获得相关各种嗯哼:
zoomquiet


自怼圈/年度番新

DU22.4
关于 ~ DebugUself with DAMA ;-)
粤ICP备18025058号-1
公安备案号: 44049002000656 ...::