為什麼需要理論#
再完整的模式清單也無法涵蓋程式設計中遇到的每一種情況。當沒有現成的模式適用時,你需要通用的方法來應對獨特的問題。這就是學習程式設計理論的理由之一。
學習理論的其他好處:
- 既知道「做什麼」又知道「為什麼」,帶來掌控感
- 同時涵蓋理論和實踐的程式設計對話更加有趣
三層結構:價值觀、原則、模式#
每個模式都帶有一點理論,但在程式設計中還有更大、更普遍的力量在起作用。這些橫切關注點(Cross-cutting Concerns)分為兩類:
- 價值觀(Values):程式設計的普遍主題——溝通(Communication)、簡單(Simplicity)、彈性(Flexibility)。這些價值觀影響每一個程式設計決策。
- 原則(Principles):不如價值觀那麼廣泛,但每個原則被許多模式所表達。原則在價值觀(普遍但難以直接應用)和模式(清晰可用但具體)之間架起橋樑。
這三個元素形成一種開發風格的平衡表達:
- 模式描述「做什麼」
- 價值觀提供「動機」
- 原則幫助將動機轉化為行動
flowchart TD
subgraph Values["價值觀 Values"]
V1["溝通\nCommunication"]
V2["簡單\nSimplicity"]
V3["彈性\nFlexibility"]
end
subgraph Principles["原則 Principles"]
P1["局部影響"]
P2["最小化重複"]
P3["邏輯與資料放在一起"]
P4["對稱"]
P5["宣告式表達"]
P6["變動頻率"]
end
subgraph Patterns["模式 Patterns"]
M1["具體的程式設計實踐"]
end
V1 & V2 & V3 --> Principles
Principles --> Patterns這裡呈現的是一種開發風格,而非唯一的開發風格。不同的價值觀和原則會導向不同的風格。將程式設計風格拆解為價值觀、原則和實踐,有助於更有建設性地討論分歧——如果我們對原則就有不同看法,那爭論大括號放哪裡是解決不了根本問題的。
價值觀(Values)#
三個與卓越程式設計一致的價值觀:溝通、簡單、彈性。這三者偶爾衝突,但更常互補。最好的程式提供許多未來擴展的選項、不含多餘元素、且易於閱讀和理解。
溝通(Communication)#
當讀者能理解、修改或使用程式碼時,程式碼就溝通良好。
寫程式時容易只想到電腦,但當你也為他人著想時,好事就會發生:
- 程式碼更乾淨、更易讀
- 更具成本效益
- 思維更清晰
- 獲得新鮮的視角
- 壓力降低
- 滿足部分社交需求
作者提到 Knuth 的 Literate Programming 的啟發:程式應該像一本書一樣可讀,有情節、節奏和令人愉悅的小巧修辭。作者與 Ward Cunningham 嘗試將 Smalltalk 中最乾淨的程式碼(ScrollController)改寫成故事,結果發現每當一段邏輯難以解釋時,重寫程式碼比解釋為什麼難以理解更容易。溝通的需求改變了他們對編程的看法。
聚焦溝通有堅實的經濟基礎:
- 軟體的大部分成本發生在首次部署之後
- 修改程式碼時,閱讀既有程式碼的時間遠多於撰寫新程式碼
- 因此,要讓程式碼便宜,就該讓它易讀
聚焦溝通也能改善思維:
- 思考「別人會怎麼看這段程式?」會啟動不同的神經元,讓你從孤立的視角中退一步
- 知道自己正在做正確的事,壓力會降低
- 作為社群導向的物種,明確考慮社交因素比假裝它們不存在更符合現實
簡單(Simplicity)#
Edward Tufte 在《The Visual Display of Quantitative Information》中有個練習:取一張圖表,開始刪除所有不增加資訊的標記,結果得到一張新穎且更易理解的圖表。
消除多餘的複雜性能讓讀者更快理解程式:
- 有些複雜性是本質的,準確反映了待解決問題的複雜度
- 有些複雜性是偶然的——我們掙扎著讓程式跑起來時留下的痕跡
- 正是這種多餘的複雜性降低了軟體的價值:使軟體不太可能正確運作,且更難成功修改
簡單是相對的:
- 對專家程式設計師來說簡單的東西,對初學者可能極其複雜
- 好的散文是為讀者寫的,好的程式也是為讀者寫的
- 稍微挑戰讀者沒問題,但太多複雜性會讓他們迷失
計算的歷史是複雜化與簡化的交替浪潮:
- 大型主機架構越來越巴洛克,直到迷你電腦出現
- 程式語言也經歷同樣的循環:C 產生了 C++,C++ 產生了 Java,而 Java 本身也越來越複雜
- 追求簡單能促進創新——JUnit 比它替代的測試工具簡單得多,因此催生了各種衍生工具和新的測試技術
在所有層次上應用簡單:
- 格式化程式碼,使每個元素都不能刪除而不損失資訊
- 設計時沒有多餘元素
- 挑戰需求,找出真正本質的部分
溝通和簡單通常相輔相成:越少多餘的複雜性,系統越容易理解;越注重溝通,越容易看出哪些複雜性可以丟棄。在罕見的情況下,簡化反而讓程式更難理解時,作者選擇溝通優先於簡單。
彈性(Flexibility)#
在這三個價值觀中,彈性是被用來為最多無效的編碼和設計實踐辯護的理由。例如,為了取得一個常數,作者見過程式去查詢一個環境變數來獲取目錄名稱,再在該目錄的檔案中找到常數值。為什麼這麼複雜?因為「彈性」。
- 程式應該有彈性,但只在它們實際會變化的方面
- 如果常數永遠不變,所有那些複雜性就是成本而無收益
關於彈性的關鍵認知:
- 軟體大部分成本在首次部署後產生,所以程式應該易於修改
- 但你今天想像明天需要的彈性,往往不是你修改程式碼時真正需要的
- 簡單性和完善測試帶來的彈性,比投機式設計帶來的彈性更有效
選擇能鼓勵彈性且帶來即時利益的模式。對於有即時成本但只有延遲利益的模式,通常耐心等待是最佳策略——等到真正需要時再應用,就能精確地按需要的方式使用。
彈性與其他價值觀的關係:
- 彈性可能以增加複雜性為代價(例如使用者可配置選項)
- 簡單可以促進彈性——如果能找到方法在不損失價值的情況下消除可配置選項,程式將更容易在未來修改
- 增強軟體的可溝通性也增加彈性——能快速閱讀、理解和修改程式碼的人越多,組織未來變更的選項就越多
原則(Principles)#
實作模式不是「就是這樣」的。每個模式都表達了溝通、簡單、彈性這些價值觀中的一個或多個。原則是另一層通用概念,比價值觀更具體、更貼近程式設計,同時也是模式的基礎。
檢視原則的價值:
- 清晰的原則能催生新的模式,就像元素週期表催生了新元素的發現
- 原則能為模式背後的動機提供解釋,連結到通用而非特定的概念
- 矛盾模式之間的選擇,通常最好以原則來討論
- 理解原則能在遭遇新情境時提供指引——例如學習新程式語言時,用原則發展出有效的風格
局部影響(Local Consequences)#
組織程式碼使變更只產生局部影響:
- 如果這裡的修改會導致那裡的問題,變更成本將大幅上升
- 具有局部影響的程式碼能有效溝通——可以逐步理解,不需要先組裝出對整體的理解
- 因為保持低變更成本是實作模式背後的主要動機,局部影響原則是許多模式推理的一部分
最小化重複(Minimize Repetition)#
有助於保持影響局部性的原則是最小化重複:
- 當同一段程式碼存在於多處時,修改一份就必須決定是否修改所有其他副本——變更不再是局部的
- 副本越多,變更成本越高
重複不只是複製程式碼:
- 平行的類別階層也是重複——一個概念性的修改需要改動兩個或更多類別階層,影響就擴散了
- 重複有時要到建立後才會被察覺,即使察覺了也不一定能立刻想到好的消除方法
- 重複不是邪惡的,它只是提高了變更的成本
消除重複的一種方式是將程式拆分為許多小片段:
- 小陳述句、小方法、小物件、小套件
- 大塊的邏輯傾向於重複其他大塊邏輯的部分
- 清楚溝通程式的哪些部分相同、哪些僅相似、哪些完全不同,使程式更易讀、更便宜修改
邏輯與資料放在一起(Logic and Data Together)#
將邏輯和它操作的資料放在一起——盡可能在同一個方法中,或同一個物件中,至少在同一個套件中:
- 進行修改時,邏輯和資料很可能需要同時改變
- 如果它們在一起,變更的影響就會保持局部
一開始不一定能看出邏輯或資料該放在哪裡。可能在寫 A 的程式碼時才發現需要 B 的資料。程式跑起來後才注意到程式碼離資料太遠。此時可以選擇:把程式碼搬到資料那邊、把資料搬到程式碼這邊、把兩者放進一個輔助物件(Helper Object),或者暫時接受無法有效合併的現實。
對稱(Symmetry)#
程式中處處充滿對稱:
add()方法伴隨remove()方法- 一組方法都接受相同的參數
- 一個物件中所有欄位有相同的生命週期
辨識並清楚表達對稱使程式碼更容易閱讀——一旦讀者理解對稱的一半,就能快速理解另一半。
程式碼中的對稱是概念性的而非圖形的:同一個想法在程式碼中每次出現都以相同方式表達。
以下是一個缺乏對稱性的範例:
void process() {
input();
count++;
output();
}第二行比其他兩行更具體。基於對稱性重寫:
void process() {
input();
incrementCount();
output();
}但這仍然違反對稱——input() 和 output() 以意圖命名,incrementCount() 以實作命名。進一步思考為什麼要遞增計數:
void process() {
input();
tally();
output();
}找出並表達對稱性通常是消除重複的先導步驟——如果類似的想法存在於程式碼的多處,讓它們彼此對稱是統一它們的好起點。
宣告式表達(Declarative Expression)#
盡可能以**宣告式(Declarative)**表達意圖:
- 命令式程式設計強大而靈活,但閱讀時需要跟隨執行的脈絡,在腦中建立程式狀態和控制流的模型
- 對於更像簡單事實、沒有順序或條件的部分,宣告式的程式碼更容易閱讀
以 JUnit 為例:
舊版本中,類別可以有一個靜態 suite() 方法回傳要執行的測試集合:
public static junit.framework.Test suite() {
Test result= new TestSuite();
...complicated stuff...
return result;
}要知道哪些測試會被執行,必須去閱讀並理解這個方法。
JUnit 4 使用宣告式表達解決同樣的問題:
@RunWith(Suite.class)
@TestClasses({
SimpleTest.class,
ComplicatedTest.class
})
class AllTests {
}只需查看 @TestClasses 註解就能知道哪些測試會被執行。宣告式風格讓程式碼更容易閱讀,因為不需要懷疑有什麼刁鑽的例外。
變動頻率(Rate of Change)#
將變動頻率相同的邏輯或資料放在一起,將變動頻率不同的分開。這是一種時間對稱(Temporal Symmetry)。
以稅務軟體為例:
- 將一般稅務計算的程式碼與特定年度的程式碼分開
- 修改下一年度的程式時,希望確保前幾年的程式碼仍然有效
- 分離它們能讓你對變更的局部影響更有信心
變動頻率也適用於資料。一個物件中的所有欄位應以大致相同的頻率變動:
- 只在單一方法啟動期間修改的欄位應該是區域變數
- 兩個一起變動但與鄰近欄位不同步的欄位,可能屬於一個輔助物件
setAmount(int value, String currency) {
this.value= value;
this.currency= currency;
}演化為:
setAmount(int value, String currency) {
this.value= new Money(value, currency);
}最終:
setAmount(Money value) {
this.value= value;
}value 和 currency 兩個原始欄位是對稱的——它們同時變動,但與物件中的其他欄位不對稱。透過將它們放入自己的物件來表達這種對稱,向讀者傳達了它們的關係,也可能為進一步減少重複和局部化影響創造機會。
結論#
本章介紹了實作模式的理論基礎:
- 價值觀:溝通、簡單、彈性——為模式提供廣泛的動機
- 原則:局部影響、最小化重複、邏輯與資料放在一起、對稱、宣告式表達、變動頻率——幫助將價值觀轉化為行動
mindmap
root((實作模式理論))
價值觀
溝通
簡單
彈性
原則
局部影響
最小化重複
邏輯與資料放在一起
對稱
宣告式表達
變動頻率接下來的「Motivation」章節描述了使聚焦於透過程式碼溝通成為有價值活動的經濟因素。