物件的三條法則#

借用 Asimov 的《I, Robot》三定律,作者提出物件三法則,可同時套用在類別與整體程式:

  1. 物件應做其方法所稱之事(do what its methods say it does)
  2. 物件不應造成傷害(do no harm)
  3. 物件無法執行所請求操作時應通知使用者(notify if unable to perform)

第一定律:方法名稱要等於行為#

呼應 A Rose by Any Other Name Is Not a Rose 與最少驚奇原則(Principle of Least Surprises)。

  • removedelete 在多數 UI 中含義不同:remove 通常只是從集合移除,delete 才是徹底刪除。
  • 自定義方法的語意要與這類常識保持一致,避免讀者誤解。

第二定律:不要佔用不必要的資源#

範例:早期 GUI widget 在建構時開設定檔,銷毀時才關閉。系統限制同時開檔 20 個 → 第 21 個 widget 創不出來。

  • 物件需要學會「自己生活在自己能力範圍內」,及時釋放資源。
  • 若語言不保證何時銷毀(如 Java),應提供 dispose() 等方法手動釋放。
  • 設計協作要相互尊重——不要把鄰居物件的資源也一起霸佔。

第三定律:不要沉默失敗#

Never Be Silent——遇到錯誤就要報錯,不要默默吞掉。

  • add() 發現要加入的元素已存在,就該報錯而不是默默覆蓋。
  • 報錯方式(例外或回傳碼)依語言與團隊指南而定,重點是「呼叫端有機會知道」。

內聚與耦合#

Honor the Class Maxims——讓類別保持高內聚、低耦合。

內聚(Cohesion)#

  • 每個類別只代表一個抽象。一句話說不清楚,就是混了多個概念。
  • 範例:Time 同時代表「時鐘時間」與「時間區間」時,加減秒數會出現逾界問題;先寫一句話定義就能避免。

耦合(Coupling)#

  • 緊耦合:依賴對方實作,是要避免的。
  • 共同耦合:透過介面互動,實作換了不必動上層。
  • 鬆耦合:只仰賴對方存在。

多數情況追求 common 或 loose——「Design to an interface, not an implementation.」

方法的歸屬#

Place Methods in Classes Based on What They Need——若方法不需實例資料就不該屬於該類別;若它只需實例資料就該屬於該類別。

範例:break_into_lines(text, charsPerLine) 不操作 CDRelease 屬性,三個歸屬選擇:

  • 工具類別StringHelper.break_into_lines(...),純函式取向。
  • String 衍生類別MyString.break_into_lines(...),物件能自我處理但容易塞滿不相關方法。
  • 保留在原類別:易於閱讀但違反原則,且難以重用。

多型:繼承還是介面#

多型(polymorphism)有兩種用途:

  • 同介面、同行為、不同實作(例如印表機驅動)。
  • 同介面、不同行為(例如 Proxy 類別)。

實踐方式:

  • 繼承:衍生類別共享基底類別介面。
  • 介面:在 Java/C# 中以多介面表達多重關係。
  • Duck Typing(Ruby 等):第三條路,只要方法簽章一致就能用。

Avoid Premature Inheritance——繼承需要時間演化。

Think Interfaces, Not Inheritance——介面提供更靈活的關係。

抉擇:

  • 「需要 switch 判斷類別行為」是繼承時機的訊號;多處出現相同 switch 更該用繼承。
  • SquareEquilateralTriangle 都是 RegularPolygon——用介面比硬塞繼承更恰當。
  • 委派比繼承更強:把行為委派給策略物件(Strategy 模式),就能避免類別爆炸。

Figure 6-1. Shapes using inheritance

Figure 6-2. Shapes using an interface

Figure 6-3. Shapes using an additional interface

Figure 6-4. A framework using strategy

不要過度分類(Don’t Overclassify)#

Don’t Overclassify——根據行為而非資料來分離概念。

範例:CDRelease 有 NewRelease、GoldenOldie、Regular 三類。

  • 不同類別:建立 NewReleaseCDGoldenOldieCDRegularCD 三類,繼承 CDRelease
  • 同類別不同物件:在 CDRelease 中存一個 CDCategory 屬性,行為差異透過查表處理。

判斷準則:

  • 三類差別只是值(base rental period 為 2、3、4 天)→ 一個類別 + 列舉就夠。
  • 若行為差異很多(不只是值),才考慮繼承。

Figure 5-1. A CDRelease inheritance hierarchy

Figure 5-2. CDRelease using an enumerated category

行為若真的不同,才回到繼承;其後又能進一步用委派取代繼承:

Figure 5-3. CDRelease with inheritance

Figure 5-4. CDRelease with delegation

政策與實作分離#

Separate Policy from Implementation——把 what 與 how 拆開,what 才會易讀易維護。

寫程式建議流程:

  1. 先寫意圖(policy):if (a_customer.is_good_customer()) a_customer.provide_discount();
  2. 再實作 is_good_customer()provide_discount() 的判斷與邏輯。

一件事做好就好#

Do a Little Job Well and You May Be Called Upon Often——專注做小事的方法/類別更易被重用。

借鑒 Unix 哲學:

  • wc 計算行數、字數、字元數;ls 列檔案;ls | wc 算出檔案數。
  • File 只做基本讀寫;FileWithLines 用於按行讀;FileWithKeywords 用於 keyword 取值。各自負責一件事,靠委派組裝。

顯式命名與避免重載#

Overloading Functions Can Become Overloading——使用獨特名稱讓函式自我描述。

不要:

search(int an_int)
search(String a_string)

應該:

search_for_first_name(CommonString a_string)
search_for_last_name(CommonString a_string)

理由:

  • 文字搜尋「search(」會找到所有版本,難以鎖定特定方法。
  • 同一型別下的不同搜尋語意,無法共存於同名重載。
  • 運算子重載也是同理:Time t + 5 看不出加的是時、分、秒,改 t.add_hours(5) 立刻清楚。

偏離與例外的程式內結構#

例外應分為兩個階層:

  • 預期偏離:使用者可採取行動,例如 CustomerIDNotFoundException
  • 意外錯誤:應終止或上報,例如 DatabaseUnreachableException

在 Java/C# 中,checked exceptions 強制呼叫端處理,適合預期偏離;unchecked exceptions 適合「應終止」型錯誤。

層次處理:

  • 低層拋具體例外(ChainBrokenException)。
  • 中層轉成抽象例外(UnableToDeliverException),上層只看抽象。
  • 訊息中保留底層細節作為說明,呈現給使用者時再轉成可採取行動的描述。

用狀態思考物件行為#

物件常擁有狀態,行為依當下狀態而變。

See What Condition Your Condition Is In——用狀態為基礎分析物件行為。

範例:

  • FileNotOpenOpenForReadingOpenForWritingOpenForReadingAndWriting
  • CDDiscRented / NotRentedInService / Lost
  • CustomerRegularInactiveNeverAgain

實作選擇:

  • 二元狀態:用布林屬性或「是否擁有 Rental」即可。
  • 多狀態:用列舉屬性(enumeration CustomerState)。
  • 狀態相依的行為較多:採用 State 模式,把行為委派給狀態物件,可在執行期切換狀態而不必改類別。

Figure 9-2. CDDisc state diagram

Figure 9-3. CDDisc service state diagram

Figure 9-4. Customer with states

不要把新狀態硬塞入現有狀態。電子機票案例中,「ticketed」概念若只剩下「是否被列印」就無法描述新流程,最終讓使用者鑽漏洞。

完整覆蓋所有狀態#

當設計新狀態時,要主動檢查:

  • RentalCurrent/CompletedOverdue/Late 是兩個正交軸:
    • Current × Overdue / NotOverdue:動態,會因時間經過而變。
    • Completed × Late / NotLate:靜態,租借結束後不再改變。
    • Completed 不會 OverdueCurrent 不會 Late

用狀態機分析時,AT&T 早期交換機案例的設計者花了數月窮舉所有狀態與轉換,最後讓編碼者幾週內完成;這種前期投資撐起多年的可維護性。

程式內結構指南總結#

指南訊息
Three Laws of Objects做名實相符、無害、有錯就報
Honor the Class Maxims高內聚、低耦合
Place Methods in Classes Based on What They Need看資料需求決定方法歸屬
Avoid Premature Inheritance繼承需要時間演化
Think Interfaces, Not Inheritance介面比繼承更靈活
Don’t Overclassify依行為而非資料分類
Separate Policy from Implementationwhat 與 how 分離
Do a Little Job Well一件事做好,重用更容易
Overloading Functions Can Become Overloading用獨特名避免重載
Never Be Silent不要沉默失敗
See What Condition Your Condition Is In用狀態為基礎分析行為