pragmaticProgrammer

While You Are Coding - 當您寫程式時

千呼萬喚 終於等到Pragmatic Programmer 20週年紀念版 如果沒聽過這本書 你大概也聽過程序員修煉之道︰從小工到專家這本暢銷了20年的書 終於等到了再版

在再版裡面 刪掉了比較過時的內容和範例 收集了20年來收到的feedback 在讓這本書的內容也可以適用於2020年的程序員 但在我細細品嚐後發現 其實很多人生的哲學並不是只適用於程序員 各行各業看了都可以有所收穫

因為每個篇章的篇幅都不長 所以筆記也用條列式紀錄

本篇的圖片以及程式碼來自於原書內容

第七章: 當您寫程式時

聆聽你的蜥蜴腦

人類有三種腦 分別是蜥蜴腦 猴子腦以及人類腦 蜥蜴腦掌管習慣性的動作 猴子腦掌管情緒 人類腦思慮周延且能理性有效地解決問題 —- Rory Miller

直覺是我們大腦潛意識對模式的一種反應 有些是天生的 有些是透過重複學習而習得的 日常生活中的直覺也是一樣 看到某些可能危險的東西不去靠近 都是生物演化的本能

身為一名程式設計師 當你累積了經驗 你的大腦就會逐漸形成一層又一層的隱性知識 包含某些東西可能沒有用 某些東西可能很好 某些東西可能導致錯誤 或者某件事情實在太麻煩了 不應該這麼做

直覺不會說話 你必須要感覺到直覺的發生 然後找出發生的原因 因為這是你身為工程師的經驗的總結 用最快的速度給你反饋

怎麼跟蜥蜴腦交談呢

當你的直覺告訴你這件事不行或是可行 但你卻不知道明確的原因時 你必須要想辦法跟你的蜥蜴腦交談

第一個方法 就是停止你正在做的事 給自己時間空間做些不動腦的事 遠離螢幕遠離鍵盤 讓想法滲透到你腦中的各個層面 最終也許那些直覺就會上升到有意識的層級 你就會知道你產生直覺的真正原因

第二個方法 就是把問題外部化(Externalizing the issue) 把你正在寫的程式碼畫個圖出來 或是向你的同事解釋一下 讓你大腦的不同部分接觸這個問題 相信每個人都有經驗 把你自己的問題跟同事講解到一半的時候 就突然頓悟問題出在哪了

要是這樣都還是找不到 就第三個方法 原型設計(prototyping)

如果現在已經有現成的程式碼了 把它收起來 當作沒有 去做原型設計 去理解你原本想要去探索的某些方面 比如你使用的framework如何做data binding 或是一個新的演算法 你想知道它如何處理邊界條件 或是你想去嘗試不同的user interaction 都可以使用原型

原型製作完成之後 開始照著原型寫程式碼! 寫著寫著你很可能就會找出當初你直覺發生的原因

   

靠巧合寫程式

記不記得以前我們看戰爭片的時候 一個士兵在灌木叢中小心翼翼地前進 前面的一塊空地可能有地雷 於是士兵就小心翼翼的戳前面的地面 邊戳邊走 走了一陣子發現都沒爆炸 於是覺得這塊地沒問題 然後昂首向前走的時候就踩到地雷了

這個士兵當初對地雷的探測沒有爆 純粹是運氣好 卻因為運氣好導致了一個錯誤的災難性結論

身為開發者 我們也是在地雷區工作 每天有上百上千個陷阱等著我們 我們應該避免利用巧合 而是謹慎的寫程式

何謂靠巧合寫程式

我們常常會遇見實作中的偶然情況 單純是因為程式碼的撰寫方式讓你可以依賴邊界條件或是沒人了解的意外情況

假設你在呼叫一個函式 傳入了錯誤資料 這個函式以特定的方式回應 你就以這個回應繼續寫你的程式碼 但該函式的作者根本就不是預期要那樣子回應你 等到那個函式修復完成後 你的程式碼崩潰 但是你怎麼看都覺得你的程式應該要正常運作 永遠找不到錯在哪

比如說Fred想要在螢幕上顯示一些東西

paint();
invalidate();
validate();
revalidate();
repaint();
paintImmediately();

這些函示並不是被設計成以這樣的順序被呼叫的 雖然這麼跑起來結果看起來對 但那只是湊巧

最糟糕的是 因為跑起來對 Fred就不冒著把跑起來正確的程式搞壞的風險去修改它 但身為一個務實工程師 你必須要思考

1.它可能沒有真正發揮作用 可能只是看起來有作用

2.你所依賴的邊界條件只是一個意外 在不同的環境中(螢幕解析度 CPU數量) 跑起來結果可能不同

3.未按照設計使用的行為 可能在函式庫更新下一個版本的時候 你的程式就會改變了

4.額外且不必要的呼叫會讓你的程式變慢

5.額外的呼叫增加了引入新bug的風險

相關並不表示因果關係 Correlation does not imply causation

人類天生就擅長發現模式和原因 比如俄羅斯領導人總是禿頭跟沒禿頭交替領導 雖然你寫程式不會依賴於這個假設 但現實生活很多明明就是獨立的事件卻讓你感覺彼此有關聯

但相關並不表示因果關係 簡單的例子就是

“電視看多的小孩大多比較暴力 所以看電視會導致暴力” 這句話的因果關係很可能是錯的 因為你也可以解讀成 暴力的人比較喜歡看電視

比如說一個log檔案顯示 每1000個請求就有一個間接性錯誤 這個錯誤可能是race condition 也可能是bug 只在測試伺服器上發生 這可能是不同環境之間的巧合 也可能是真的有bug

不要假設 找出為什麼並且證明

如何謹慎的寫程式

我們希望花費更少的時間來寫程式碼 希望儘早的捕獲開發時期的錯誤 這需要謹慎的開發程式

1.時刻注意自己在做什麼

2.你能像一個初階工程師解釋你的程式碼嗎 如果不能 那你可能就是在靠巧合寫程式

3.不要在黑暗中寫程式碼 比如寫一個你沒完全掌握的應用 或是使用一個你不理解的技術 畢竟如果你不知道為什麼一個東西可以正常工作 你就不會知道一個東西為什麼會出錯

4.從做計畫開始 不管是計畫在腦子裡 還是是在白板上

5.把你的假設記錄下來

6.不要只是測試你的程式碼 還要測試你的假設

7.優先考慮要把心力放在哪裡 如果沒有正確的架構 那程式再華麗都沒用

8.不要讓現在的程式碼支配未來的程式碼 如果程式碼不再合適 所有的程式碼都是可以替換的 隨時做好重構的心理準備

總之 下次當有件事情看起來可行 但你不知道為什麼 請確保這不是一個巧合

重構

重構就是重組現有的程式碼主體 改變內部結構而不改變外部行為的技術

基本上講的都是重構 - 改善既有程式的設計的東西

測試對程式碼的意義

測試的目的不是要找出bug

測試的好處是發生在你思考和撰寫程式碼時 而不是執行測試的當下

測試驅動開發

TDD(Test-Driven Development)的基本循環為

1.決定要添加的一小部分功能

2.寫一個測試專門測試該功能

3.執行所有測試 確認唯一的失敗是剛剛寫的測試

4.撰寫使測試通過的最少程式碼 並驗證剛剛寫的測試通過了

5.重構程式碼 別忘了邊重構邊確保測試一直通過

這個週期應該非常短 大概幾分鐘一個週期 這樣你就可以不斷的寫測試 讓他們work

測試的文化

你撰寫的所有軟體都將被測試 如果不是被你或你的團隊測試 就是被你的客戶測試 既然如此 你不如就提前進行徹底的測試 來減低維護成本和減少客服的抱怨電話

對待測試就像對待其他任何的產品一樣 需要保持去耦合性 簡潔性和健壯性 不要依賴不可靠的東西 比如UI中某個元件的絕對位置 或是伺服器log的某個timestamp

Testing, design, coding — it’s all programming.

以屬性為基礎的測試

我們常在寫程式碼跟測試的時候 都是基於自身針對測試事物的知識來寫 所以其實有可能你理解錯誤 你就寫出了錯誤的程式碼以及測試 而你卻因為測試通過而產生了信心

本書建議可以除了單元測試之外 還需要加上針對屬性的測試(Property-Based Testing): 就是找出程式碼必須遵守的合約(Contracts) 找出程式碼的不變量(Invariants) 這些都應該加在自動化測試裡面

比如說排序完的陣列跟排序前長度應該相同(不變量) 或是任何元素都不會大於後面的元素(合約)

待在安全的地方

每天的新聞都充斥著毀滅性的資料洩漏 數億美元的花費需要用來補救之前工程師犯的錯誤 而大多數情況 會發生這些事並不是因為攻擊者特別厲害 純粹是開發人員太粗心了

你以為的90%

再繼續這個章節之前 我們必須把你的概念導正

在你寫程式碼時 你會經歷很多次

(It works) -> (Why it is not working) 的循環 直到最後你會說 太好了終於全部完成了的階段

從零 到程式碼成功跑起來了 你以為已經完成了90% 事實上 你只完成了50%

另外的一半 是需要你分析程式碼 找出可能出錯的地方 把這些可能出錯的地方都加入到測試 你要考慮傳遞錯誤的參數 誤用或不可用的資源 使用者是無意還是故意的將系統搞砸等等

安全基本原則

務實的工程師是偏執狂 我們知道外部的攻擊者會抓住我們留下的任何漏洞來破壞系統 你必須時刻謹記

1.最小化攻擊總面積

2.最小權限原則

3.安全預測值

4.對敏感性資料加密

5.維護安全更新

最小化攻擊總面積

一個系統的攻擊總面積 是指攻擊者可以輸入資料 取得資料或呼叫服務執行的所有存取點的加總

程式碼越複雜會使得攻擊表面積更大 程式碼越簡單代表更少的bug 更容易發現潛在的弱點

永遠不要信任來自外部的資料 再將這些資料傳到資料庫或是其他處理前一定要做好消毒工作

今天一個簡單的shell

puts "Enter a file name to count: " 
name = gets
system("wc -c #{name}")

要是使用者這樣輸入 就安心上路

test.dat; rm -rf /

如果你的服務不需要驗證身份 那代表地球上的任何使用者都可以呼叫你的服務 那最簡單的攻擊就是DDOS

即使你將授權可以使用的使用者保持在最小數量 但許多網路設備或是使用者用的都是預設的或是最簡單的密碼 而一個擁有發佈權限的的帳戶被盜用的話 整個產品都可能被破壞

不應該輸出使用者沒有權限看到的東西 比如說 “The password you entered is used by another user”

如果使用者在ATM或是網頁上看到一個完整的stacktrace 就很可能被攻擊 這些當初為了除錯而設計的資訊 都需要徹底隱藏

最小權限原則

永遠只提供最小的權限給使用者 並且在用完後就馬上收回

安全預設值

應用程式或網站上的使用者的預設設定應該要是最安全的值 雖然這可能不是最好用的或是最方便的值 但是最好讓每個人決定安全性和方便性的權衡

比如說 在使用者輸入密碼的時候 預設是顯示星號 但同時也給使用者顯示輸入的密碼的選項

對敏感性資料加密

不要把個人識別資訊 財務資料 密碼等等的資料直接存在資料庫或其他外部檔案

也不要把API金鑰 SSH金鑰 加密密碼等等的資訊交到版本控制裡 金鑰和密碼通常要跟程式碼分開管理 放在設定檔或環境變數中

維護安全更新

更新電腦系統很痛苦 但資料外洩或安全性降低更痛苦 所以應該儘早套用安全更新

命名

名字的意義是什麼呢 對於工程師來說 就是一切

我們為應用程式 子系統 模組 函式 變數建立名稱 我們不斷建立新事物並給他們命名 這些名字非常非常重要 因為他們透露著意圖

來看幾個例子

1.我們是個珠寶商 我們正在對存取我們網站的人進行驗證

let user = authenticate(credentials)

變數的名稱是user 那就基本上沒啥意義 如果是叫customer或是buyer都更好一點 好的命名可以時時刻刻提醒著我們 我們想做什麼 這代表什麼

2.我們可以對訂單進行折扣

public void deductPercent(double amount)

這個命名有兩個問題 deductPercent指的是他做什麼事 而不是他為什麼做這件事 再來 amount是一個絕對數量還是一個百分比呢?

不如我們命名成這樣

public void applyDiscount(Percentage discount)

這個命名直接的表達了意圖 並且使用這可以去看Percentage的資料結構去得知到底要傳入0~1的數還是0~100的數

尊重文化

There are only two hard things in computer science: cache invalidation and naming things

某些語言習慣在迴圈裡面用i, j, k 等等的暫時變數 但在另外的語言或環境中就不推崇

某些語言社群推崇cacelCase 但也有些推崇snake_case 當你在新增程式碼的時候 請尊重社群的文化