到處 new 具體類別、要換實作得改一堆地方,怎麼重構?

Medium★★★★Studied

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

Concepts practiced🏭 工廠方法 (Factory Method)📏 依賴反轉原則 (DIP)📏 開放封閉原則 (OCP)

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

📚 From the Books

到處 new 具體類別、要換實作得改一堆地方,怎麼重構?ProblemMedium重構情境

業務程式一邊跑邏輯,一邊在 if-else / when 裡決定要 new 哪個具體類別,同樣的建立碼還在好幾個呼叫端重複出現。要換實作或加一種型別,就得回頭改一堆地方。這不是「該建哪個行為」的問題,而是「物件由誰、在哪裡被建立」的問題——怎麼重構?

🎯 Scenario

interface Repo { fun insert(o: Order) }
class SqlRepo : Repo { override fun insert(o: Order) { /* ... */ } }
class FileRepo : Repo { override fun insert(o: Order) { /* ... */ } }

class OrderService {
    fun save(o: Order) {
        val repo = SqlRepo()   // 使用方直接 new 具體類別,綁死實例化策略
        repo.insert(o)
    }
}

class ReportJob {
    fun run() {
        val repo = SqlRepo()   // 同樣的建立碼又出現一次
        // ...
    }
}

// 或者:依型別建立的 when 散落各處
fun makeRepo(type: String): Repo = when (type) {
    "sql"  -> SqlRepo()
    "file" -> FileRepo()
    else   -> error("unknown repo: $type")
}

壞味道:使用方一邊執行業務邏輯、一邊在分支裡決定 new 哪個類別,把「使用」和「建立」兩件事混在一起,凝聚變弱、過早綁死在某種實例化策略;同樣的建立知識重複散落在多個呼叫端。

🧠 Intuition

TIP

先別看下面。把問題切成兩半:使用方真正需要的是抽象介面Repo),還是某個具象類別SqlRepo)?如果「決定 new 哪一個」是唯一容易變化的部分,能不能把這個決定從使用方抽出去、集中管理,讓使用方眼裡只剩抽象?

⚖️ Reference Design

這頁是「建立」的散落,不是「行為」的分支

先劃清界線,別跟 以多型取代型別 switch 混淆:

  • 物件導向世界裡,「使用其他物件」與「建立/管理其他物件」是兩件不同的事。新手常把兩者混在一起——程式一邊跑業務邏輯、一邊在 if-else 裡決定 new 哪個類別。
  • 一條基本守則:一個物件要嘛使用其他物件,要嘛建立/管理其他物件,不能同時兩者。
  • 所以判準是:分支決定的是「該執行哪種行為」→ 那是多型 / Strategy;分支決定的是「該實例化哪個類別」→ 那是工廠。本頁處理後者。
第一步:Simple Factory —— 把建立集中到一處

把「該實例化哪一個子類別」這個容易變化的決定,從使用方抽出來、封裝進一個獨立的工廠:

object RepoFactory {
    fun create(type: String): Repo = when (type) {  // switch 仍在,但只剩這一處
        "sql"  -> SqlRepo()
        "file" -> FileRepo()
        else   -> error("unknown repo: $type")
    }
}

// 使用方只認識 Repo 介面,不再認識具象類別
class OrderService(private val repo: Repo) {
    fun save(o: Order) = repo.insert(o)
}
  • 使用方不需依賴具體產品,給一個 key 就能拿到對應實例;建立知識收斂到一處,重複消失。
  • 但缺點也清楚:每新增一種產品,都要回頭改 createwhen——既對擴展開放,也對修改開放,違反開放封閉原則。
第二步:Factory Method —— 把「該建哪個」下放到子類,消掉 switch

再進一步:定義一個建立物件的介面,讓子類別決定要實例化哪一個類別,把實例化延遲到子類:

interface RepoFactory { fun create(): Repo }

class SqlRepoFactory  : RepoFactory { override fun create() = SqlRepo() }
class FileRepoFactory : RepoFactory { override fun create() = FileRepo() }

// 使用方只透過抽象工廠與抽象產品協作
class OrderService(private val factory: RepoFactory) {
    fun save(o: Order) = factory.create().insert(o)
}
  • 不再有一個含 switch 的工廠類。新增一種產品,只需新增一個產品類 + 一個工廠類,不必修改原有的工廠——整個體系對擴展開放、對修改關閉,符合 OCP,也消除了對具體類別的綁定(程式只處理 Repo 介面)。
  • 代價:每多一種產品就要多一個工廠類,開發量增加;而且「選擇要用哪個工廠」的判斷依然存在,只是從工廠內部移到了使用方那一處。
  • 這也是框架的招牌用法:框架活在抽象層次,不知也不該知具體型別,就用 Factory Method 把「具體實例化」交還給用框架的人。詳見 Factory Method
Simple Factory vs. Factory Method:何時用哪個
Simple FactoryFactory Method
分支判斷集中在工廠類的 switch下放到使用方「選哪個工廠」
新增產品要改工廠類(違反 OCP)只需新增類別(符合 OCP)
成本一個工廠類,簡單每種產品多一個工廠類
  • Factory Method 是 Simple Factory 的進一步抽象與推廣:克服了它違反 OCP 的缺點,同時保住「封裝物件建立過程」的優點。
  • 但它並沒有讓「選哪個」的判斷消失,只是搬了家。先用 Simple Factory 集中,等擴充壓力真的出現、switch 開始頻繁被改,再升級成 Factory Method。
和 DI 的關係:工廠回答「誰負責建立」
  • 規則:先決定要什麼物件,再決定如何建立它們。 工廠就是把後半段獨立出來的場域。
  • 職責一刀切:使用方只看得到抽象介面、不知具象類別;工廠方知道具象類別、卻不知如何使用。凝聚與封裝雙贏。
  • 這正呼應「對抽象寫程式」——使用方依賴 抽象(DIP) 而非具象。工廠與 依賴注入 是同一問題(「誰負責建立、如何把依賴交給使用方」)的兩種解法。額外好處:使用方對任何實作組合行為都一致,好測試

🔑 Takeaways

✍️ My Notes

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

📖 Further Reading