兩三個類別各自跑「同一套流程」——步驟順序完全一樣,只有中間一兩步的實作不同,於是整段流程被複製貼上好幾份。改流程要改每一份。要怎麼重構?
🎯 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(),但因為實際物件是OracleQT,formatConnect會解析到 Oracle 的版本。 - 模式機制細節見 Template Method。
重構手法:Extract Method → 建立基底 → 補第二個子類
不是一步到位,而是兩段式:
- 先用 Extract Method 把會變的部分抽成獨立方法(即使當下還沒要泛化)。
- 建立基底類別承載不變的流程方法。
- 再寫第二個衍生類別實作它的變化部分。
這正是「找出變動並封裝」的具體流程。搭配 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
- 「流程一致、步驟實作不同」被複製多份,是 Template Method 的標準情境:把不變骨架上提到父類的 template 方法(只寫一次),把會變的步驟抽成抽象方法交給子類。
- 落地手法是兩段式:Extract Method 抽出變動部分 → 建立基底承接不變流程 → 補下一個子類;呼應 OCP「先重構、再擴充」。
- 控制反轉走好萊塢原則:父類掌控流程、回呼子類步驟;必填用 abstract、選填用 hook。
- 邊界:步驟要獨立變動或執行期切換 → 用 Strategy;步驟差異太大、無共通骨架則兩者都不套。
No notes yet — jot your takeaways or Q&A here.