模糊(obscurity)是 2.3 節提到的兩大複雜性成因之一。
對策:把程式碼寫得顯而易見(obvious)。
「顯而易見」的意義#
- 讀者能快速讀過、不必多想,第一直覺就是正確的
- 不需要花太多時間蒐集資訊
- 顯而易見的程式碼比不顯而易見的需要更少註解
不顯而易見的程式碼則:
- 讀者需大量時間與精力理解
- 容易誤解、容易產生 bug
「顯而易見」由讀者判斷#
看別人的程式碼時,比看自己的更容易發現「不顯而易見」。
因此判斷顯而易見性最好的方法 = code review。
別人說「不顯而易見」就是不顯而易見——再清楚都不能反駁。
試著理解什麼讓它變得不顯而易見 → 學會下次寫更好的程式碼。
讓程式碼更顯而易見的技巧#
1. 好的命名(第 14 章)#
精確、有意義的名字本身就減少對文件的需求。模糊的名字逼使讀者讀程式碼推導意義——耗時又易錯。
2. 一致性(第 17 章)#
相似的事用相似的方式做 → 讀者能套用既有模式知識,不必細讀就能下安全的結論。
3. 善用空白(Judicious use of white space)#
反例:擠在一起的參數文件#
/**
* @param numThreads The number of threads that this manager should
* spin up in order to manage ongoing connections. The
* MessageManager spins up at least one thread for every open connection,
* so this should be at least equal to the number of connections you expect
* to be open at once. ...
* @param handler Used as a callback in order to handle incoming
* messages on this MessageManager's open connections. ...
*/很難分辨各參數說明的邊界、不容易看出有幾個參數。
改善:加上空白#
/**
* @param numThreads
* The number of threads that this manager should spin up in
* order to manage ongoing connections. ...
*
* @param handler
* Used as a callback in order to handle incoming messages on
* this MessageManager's open connections. ...
*/在方法內以空白分區塊#
方法主體中以空白分隔不同邏輯區塊;每個區塊上方放註解描述該區塊。
空白讓註解更顯眼。
行內空白#
for(int pass=1;pass>=0&&!empty;pass--) { // 擠在一起,難讀
for (int pass = 1; pass >= 0 && !empty; pass--) { // 加空白後一目了然
4. 註解(補強無法避免的不顯而易見)#
有時無法避免不顯而易見的程式碼。這時用註解補上缺失的資訊。
把自己放進讀者的腦袋:他可能會被什麼搞糊塗?什麼資訊能消除困惑?
讓程式碼變得不顯而易見的常見來源#
事件驅動程式設計(Event-driven programming)#
模式描述:
- 應用回應外部事件(封包、滑鼠按鍵)
- 一個模組負責回報事件
- 其他部分透過註冊讓事件模組在事件發生時呼叫某函式
事件驅動讓控制流非常難追蹤:handler 從不被直接呼叫;事件模組透過函式指標 / 介面間接呼叫,連看到呼叫點也搞不清楚到底會跑哪個 handler。
對策:在每個 handler 的介面註解中說明它何時被呼叫:
/**
* This method is invoked in the dispatch thread by a transport if a
* transport-level error prevents an RPC from completing.
*/
void Transport::RpcNotifier::failed() { ... }紅旗:不顯而易見的程式碼(Nonobvious Code)#
程式碼的意義與行為無法快速看懂——紅旗。通常代表有重要資訊未被立即呈現給讀者。
通用容器(Generic containers)#
例如 Java 的 Pair、C++ 的 std::pair:
return new Pair<Integer, Boolean>(currentTerm, false);問題:
- 元素被強塞進泛用名(
getKey/getValue) - 完全看不出它們的真實意義
不要使用通用容器。需要容器時 → 為該用途定義專屬類別 / 結構:
- 用有意義的名字
- 在宣告處附加額外文件(通用容器辦不到)
一般原則#
軟體應為「易讀」設計,而不是為「易寫」設計。
通用容器對寫的人方便,對所有後來的讀者都製造混亂。多花幾分鐘定義專用結構,換來程式碼的明顯易懂。
宣告型別與分配型別不一致#
private List<Message> incomingMessageList;
...
incomingMessageList = new ArrayList<Message>();合法(List 是 ArrayList 的父類),但讀者只看到宣告時,會誤以為它就是普通 List:
ArrayList的效能與執行緒安全特性與其他List子類不同 → 影響使用方式- 最好讓宣告型別與分配型別一致
違反讀者預期的程式碼#
public static void main(String[] args) {
...
new RaftClient(myAddress, serverAddresses);
}問題:多數應用 main 結束就 exit,但 RaftClient 建構式啟動了額外執行緒,main 結束後這些執行緒繼續運作。
對策:
- 在
RaftClient建構式的介面註解中說明這個行為 - 在 main 結尾也加一行短註解,標明應用會在其他執行緒中繼續
程式碼符合讀者預期時最為顯而易見;不符時,務必用文件補上。
結語:用「資訊」的角度思考顯而易見#
從資訊的觀點看:不顯而易見的程式碼,多半因為讀者缺乏理解它所需的某些資訊。
讓程式碼顯而易見的三條路:
- 減少所需資訊量——靠抽象與消除特殊情況
- 利用讀者已具備的知識——遵循慣例與符合預期,讓讀者不必為你的程式碼新學東西
- 透過好的命名與戰略性的註解,把重要資訊呈現在程式碼中