當一個動詞型操作被硬包成類別——尤其是只有一個方法、且資料還在別的類別裡——就出現 Imperative Abstraction 壞味道。

核心定義#

物件導向的根本是把問題領域中的「東西」(things)建模為抽象。但當設計者把「動作」(operations)擴張為類別時,會產生:

  • 類別名稱本身像動詞(ReadCreateReportDiffOffsetCalculator)。
  • 內部只定義一個方法,方法名常與類別名相同。
  • 操作的資料位於別的類別——典型的「行為與資料分離」。

這是把結構化程式設計(structured programming)穿上 OO 外衣,而不是真正的 OO。

連帶副作用:

  • 類別大爆炸,提高設計複雜度。
  • 同一份資料的相關方法被切散,內聚降低——也違反封裝與模組化原則。

常見成因#

程序式思維(procedural thinking):來自 C/其他程序式背景的開發者轉用 OO 語言時,常把資料對應為「只含 public 欄位的資料類別」,操作則放到另一群類別中——本質上是 C 風格的 struct + 獨立函式的翻版。

範例#

範例 1:報表系統的類別爆炸#

財務系統中為了報表功能,建立 CreateReportCopyReportDisplayReport 等類別,各只含一個方法。資料則放在 Report 這個「資料類別」中。

Figure 3.5: 報表生成功能的類別圖

把本應內聚於 Report 的所有方法強行散落到三個類別,只是換個地方寫程序式程式碼

範例 2:影像處理的 OffsetCalculator/SaveHandler#

ApplicationOffsetCalculator.calculateOffset() 計算兩張影像的偏移,再把結果交給 SaveHandler.saveOffset() 儲存。兩個類別都只有單一方法,是 Imperative Abstraction。

Figure 3.6: 影像處理應用程式的類別圖

建議重構#

找出(或建立)一個合適的「東西」抽象,把方法和它需要的資料一起放進去。

  • 範例 1:把 CreateReport 等類別中的方法搬回 Report 類別。

Figure 3.7: 報表生成功能的建議重構

  • 範例 2:建立 ImagePair 類別,封裝「offset 的計算與儲存」這組行為。

Figure 3.8: 影像處理應用程式的建議重構

範例 2 的 ImagePair 是否違反 SRP?實際的 I/O(開檔、連 DB)會被交給另一個類別處理;saveOffset() 僅負責呼叫——責任仍是單一的。

對品質屬性的衝擊#

  • 理解性:抽象與問題領域沒有直接對應;類別爆炸提高整體複雜度。
  • 可變更性與可擴充性:對單一概念實體的修改要動到多個類別。
  • 可重用性:要重用報表功能,必須一起搬走 CreateReportCopyReportDisplayReportReport 四個類別並調整。
  • 可測試性:多個單方法類別必須集體測試才能覆蓋一個概念。

軼事:函式分解 vs. 物件分解#

一個 Java 工具包專案在發布前的設計審查中發現 Diff 類別只含 findDiff() 方法。深入檢視後,發現整個專案幾乎所有類別都以動詞命名(generatereportprocessparse)——這是整個專案採用了「函式分解」而非「物件分解」

幾個關鍵教訓:

  • 即使是熟悉專案的成員,也容易忽略明顯的設計問題——定期由外部專家做設計審查很有幫助。
  • 一個壞味道往往是更深問題的徵兆——這個案例的根因是「程序式思維」。
  • 越接近發布,重構成本越高、風險越大。早期就要警覺

別名#

  • Operation class:本應為類別中之方法,卻被獨立成類別。

實務考量:物化(reification)#

把「非物件」提升為「物件」叫做物化。這在某些設計樣式中是有意為之的:

  • State pattern:將狀態機編碼為物件。
  • Command pattern:將請求物化為命令物件。
  • Strategy pattern:將參數化的演算法封裝為物件。

當物化是為了取得靈活性、可重用性、可擴充性而被刻意設計時,就不算壞味道