當兩個或多個抽象有相同名稱、相同實作、或兩者皆相同時,就出現 Duplicate Abstraction 壞味道。

核心定義#

兩種主要形態(也常合併出現):

  • 同名(identical name):兩個抽象名稱相同,但行為可能不相關。仍會傷害理解性;若行為也相同,問題更嚴重。
  • 同實作(identical implementation):成員定義語意上完全相同;但這份共通性沒有被抽出來。即使方法簽章不同也算數。
  • 若這種重複出現在繼承的兄弟類別之間,往往同時觸發 6.3 節的 Unfactored Hierarchy。

此壞味道直接違反 DRY (Don’t Repeat Yourself)原則:每一份知識在系統中應該有單一且明確的表示。

程式碼複製(code clones)的四種類型#

類型性質
Type 1文字幾乎相同(僅空白、排版、註解差異)
Type 2語法相同(變數名、空白、排版、註解差異)
Type 3結構相同,但有少數陳述被加/改/刪
Type 4語意相同,但實作方式不同

常見成因#

  • 複製貼上式編程:「先把功能做出來」的捷徑。
  • 臨時性維護:多年來的隨意修補留下「cruft」(程式碼基底中的雜質)。
  • 缺乏溝通:團隊成員不知道既有類別/方法,於是重新發明輪子。
  • 類別被宣告為不可繼承:例如 Java 中的 final,迫使開發者整個複製該類別來做變體。

範例#

範例 1:JDK 中的 Timer 三胞胎#

JDK 中有三個 Timer

  • javax.swing.Timer:在指定間隔執行 ActionEvent
  • java.util.Timer:將執行緒排程在未來執行(背景執行緒)。
  • javax.management.timer.Timer:發送警報通知。

三者描述都類似,新手開發者很難快速選對。

範例 2:Date 的繼承同名#

java.sql.Date 繼承自 java.util.Date且兩者同名!編譯器因為它們在不同套件而不報錯,但在同一檔案中匯入兩者會出現名稱模糊,必須手動以全限定名稱解決。

Figure 3.13: JDK 中同名為 Date 的類別

範例 3:Eclipse 的 Scanner / PublicScanner#

org.eclipse.jdt.internal.compiler.parser.Scannerorg.eclipse.jdt.core.compiler.PublicScanner 兩個類別約 3500 行幾乎完全相同。JavaDoc 直接寫明:「mirrored in public package」。

軼事:MissingResourceException 的雙胞胎#

一個 Java 專案處理第三方函式庫拋出的 MissingResourceException,已加上 catch handler 仍偶爾崩潰。深查才發現:第三方函式庫自定了一個同名的 MissingResourceException——catch 抓的是 java.util.MissingResourceException真正拋出的是另一個。 因為兩個都是 RuntimeException,編譯器不會警告。

教訓:

  • 設計問題會引發難以追蹤的 bug
  • 命名很重要——避免重名尤其關鍵,特別是設計 API 時。

建議重構#

同名 → 改名讓兩者唯一。 同實作 → 視重複比例決定:抽出共通基底類別、抽出共通元件、或直接刪除其中一個。

  • 範例 1:將 javax.swing.Timer 改為 ExecutionTimerEventTimer,將 javax.management.timer.Timer 改為 AlarmTimer,保留 java.util.Timer
  • 範例 2:將繼承改為委派——讓 java.sql.Date 包裹 java.util.Date,並改名為 java.sql.SQLDate。詳見 6.8 節 Broken Hierarchy。

Figure 3.14: java.util.Date 與 java.sql.Date 的建議重構

  • 範例 3:重複度高達 99.9%,最佳解是刪除其中一個並讓另一個與其客戶端相容,而不是套繼承。

對品質屬性的衝擊#

  • 理解性:客戶端會困惑「該用哪一個」;重複實作膨脹程式碼。
  • 可變更性與可擴充性:一處改完還要同步改所有副本。
  • 可重用性:Type 3/Type 4 的 clone 通常嵌入了脈絡特定的邏輯,難以遷移。
  • 可靠性:型別誤用導致執行期錯誤;同步修改遺漏導致 bug。

別名#

  • Alternative classes with different interfaces:做類似事但介面不同的類別。
  • Duplicate design artifacts:等價的設計工件被複製多份。

實務考量#

容納變化#

java.util.Vectorjava.util.ArrayList 介面相似,但前者執行緒安全、後者不安全——這是有意分離兩個變體,不算壞味道。

不同情境中的同名型別#

DDD(Domain-Driven Design)的 Bounded Context 模式允許不同情境中存在同名型別:

  • 大型領域無法用單一統一模型完整表達。
  • 切分為「有界情境」後,跨情境出現同名型別是可接受的

語言對「避免重複」的支援不足#

Java 泛型不支援原始型別。java.util.Arrays 中的 binarySearchsort 等方法因此重複了七次——這是語言限制造成的 Duplicate Abstraction,不是設計者的疏忽。