為什麼需要理論#

再完整的模式清單也無法涵蓋程式設計中遇到的每一種情況。當沒有現成的模式適用時,你需要通用的方法來應對獨特的問題。這就是學習程式設計理論的理由之一。

學習理論的其他好處:

  • 既知道「做什麼」又知道「為什麼」,帶來掌控感
  • 同時涵蓋理論和實踐的程式設計對話更加有趣

三層結構:價值觀、原則、模式#

每個模式都帶有一點理論,但在程式設計中還有更大、更普遍的力量在起作用。這些橫切關注點(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;
}

valuecurrency 兩個原始欄位是對稱的——它們同時變動,但與物件中的其他欄位不對稱。透過將它們放入自己的物件來表達這種對稱,向讀者傳達了它們的關係,也可能為進一步減少重複和局部化影響創造機會。

結論#

本章介紹了實作模式的理論基礎:

  • 價值觀:溝通、簡單、彈性——為模式提供廣泛的動機
  • 原則:局部影響、最小化重複、邏輯與資料放在一起、對稱、宣告式表達、變動頻率——幫助將價值觀轉化為行動
mindmap
  root((實作模式理論))
    價值觀
      溝通
      簡單
      彈性
    原則
      局部影響
      最小化重複
      邏輯與資料放在一起
      對稱
      宣告式表達
      變動頻率

接下來的「Motivation」章節描述了使聚焦於透過程式碼溝通成為有價值活動的經濟因素。