好幾個類別的流程骨架幾乎一樣、只差中間一兩步,要怎麼收?

Medium★★★★Studied

From大話設計模式Design Patterns (GoF)Design Patterns Explained

Concepts practiced🔁 範本方法 (Template Method)📏 開放封閉原則 (OCP)🔁 策略 (Strategy)

Convention: read the requirements & constraints first, sketch your own design, then expand the reference to compare.

📚 From the Books

好幾個類別的流程骨架幾乎一樣、只差中間一兩步,要怎麼收?ProblemMedium重構情境

兩三個類別各自跑「同一套流程」——步驟順序完全一樣,只有中間一兩步的實作不同,於是整段流程被複製貼上好幾份。改流程要改每一份。要怎麼重構?

🎯 Scenario

// Oracle 版:連線 → 送 CONNECT → 送 SELECT → 回傳結果
class OracleQuery {
    fun doQuery(): ResultSet {
        val conn = "CONNECT oracle://..."   // ← 只有這步不同
        send(conn)
        val sel = "SELECT ..."              // ← 只有這步不同
        send(sel)
        return fetchResult()
    }
    // send / fetchResult 兩邊一模一樣……
}

// SQL Server 版:整段流程原封不動複製,只改「格式化」那兩行
class SqlServerQuery {
    fun doQuery(): ResultSet {
        val conn = "CONNECT sqlserver://..." // ← 差異
        send(conn)
        val sel = "SELECT TOP ..."           // ← 差異
        send(sel)
        return fetchResult()
    }
}

壞味道:複製貼上後留下兩種重複——明顯的重複(完全沒改的那幾行)與流程重複(步驟順序相同、只有實作不同)。高層流程一致、低層實作有變化,卻讓每個子型別各扛一份完整流程;改流程要改多處。

🧠 Intuition

TIP

先別看下面。把兩份流程並排:哪幾行是「一起變」的差異步驟、哪些是「永遠一樣」的骨架?如果骨架只想寫一次、把差異步驟留給各家去填,這個「骨架 + 挖洞」的形狀是哪個 pattern?

⚖️ Reference Design

答案:把骨架上提到父類的 template 方法,差異步驟下放子類

在操作中定義演算法的骨架,把某些步驟延遲到子類別;子類在不改變演算法結構的前提下重新定義這些步驟。

abstract class QueryTemplate {
    // template 方法:把「不變的流程順序」寫死在父類,只寫一次
    fun doQuery(): ResultSet {
        send(formatConnect())
        send(formatSelect())
        return fetchResult()
    }

    // 會變的步驟抽成抽象方法,交給子類實作
    protected abstract fun formatConnect(): String
    protected abstract fun formatSelect(): String

    private fun send(cmd: String) { /* 不變,父類實作一次 */ }
    private fun fetchResult(): ResultSet { /* 不變 */ }
}

class OracleQT : QueryTemplate() {
    override fun formatConnect() = "CONNECT oracle://..."
    override fun formatSelect()  = "SELECT ..."
}

class SqlServerQT : QueryTemplate() {
    override fun formatConnect() = "CONNECT sqlserver://..."
    override fun formatSelect()  = "SELECT TOP ..."
}
  • 不變行為搬到超類、去除子類中的重複程式碼;把不變與可變的行為分離,子類只專注可變部分。
  • 新支援一種資料庫,只需新增一個衍生類別、照「樣板」填空,其餘不動。
  • 多型是關鍵:透過 QueryTemplate 參考呼叫 doQuery(),但因為實際物件是 OracleQTformatConnect 會解析到 Oracle 的版本。
  • 模式機制細節見 Template Method
重構手法:Extract Method → 建立基底 → 補第二個子類

不是一步到位,而是兩段式:

  1. 先用 Extract Method 把會變的部分抽成獨立方法(即使當下還沒要泛化)。
  2. 建立基底類別承載不變的流程方法。
  3. 再寫第二個衍生類別實作它的變化部分。

這正是「找出變動並封裝」的具體流程。搭配 OCP 的兩段式手法:先重構但不加新功能(讓新功能能「插」進來而不必改既有程式),再加入新功能(只動新類別)。

好萊塢原則:別呼叫我們,我們會呼叫你

Template method 產生一種反轉的控制結構,稱為 好萊塢原則(Hollywood Principle):「別打電話給我們,我們會打給你」。父類別呼叫子類別的操作,而非反過來——流程由父類掌控,子類只負責填格。

  • template method 呼叫的步驟分兩種:primitive operations(抽象操作,子類必須覆寫)與 hook operations(提供預設行為、子類可以選擇性覆寫,預設通常什麼都不做)。
  • 用 hook 取代「覆寫父類方法再手動呼叫 super」的好處:一旦忘記呼叫 super 就會破壞流程;改用父類掌控流程、子類只覆寫 hook,就能安全擴展。
取捨:什麼時候不用 Template Method、改用 Strategy
  • Template Method 透過繼承改變演算法的部分步驟(可變步驟緊密耦合、是同一流程的環節、會「一起」變,在編譯期就固定);Strategy 透過委派 / 組合改變整個演算法(代表互相獨立、可互換的演算法)。兩者是「繼承 vs. 組合」的經典對比。
  • 若可變步驟需要獨立變動、或要在執行期切換,應改用 Strategy。反過來,若把 Template Method 誤想成「幾個 Strategy 的集合」,可能被誤導去做不必要的解耦,反而失去靈活度。
  • 也不是所有重複都適用:模板方法只能消除「客觀題」式的重複;若步驟差異大、結構根本不同,就不該硬套——要看問題本質是否真有共通骨架。

🔑 Takeaways

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading