當 AI 也在改程式碼:物件導向設計原則的真正價值

物件導向設計原則
💡 TL;DR – 本文重點速覽
物件導向設計原則不是一組必須背熟的規則,而是一套用來理解系統為什麼會越來越難改的判斷框架。從類別責任混雜、擴充只能靠修改舊程式,到依賴關係失控與循環依賴,這些問題往往以壞味道的形式出現。即使在 Vibe Coding 與 AI 持續修改程式碼的時代,設計原則的價值反而更明確,因為它們能降低修改決策的不確定性,讓人與 AI 都更容易在結構清楚的邊界內進行調整,確保系統能持續演進,而不只是勉強維持。
目錄

物件導向設計原則是什麼?先別急著背名詞

物件導向設計原則,指的是在軟體系統演進過程中,逐漸整理出來的一組結構性經驗。

它們關心的不是語法細節,而是當需求反覆變動時,哪些設計選擇會讓系統比較容易調整,哪些選擇會讓風險不斷累積。

在這樣的脈絡下,常會看到 SRP、OCP、LSP、DIP 這些縮寫名詞被提及。

每一個原則都有各自的切入角度,但它們背後的目標都是同一件事:「如何讓系統在成長過程中,仍然保持可調整性」。

當程式碼生成變得很快,結構問題反而更早出現

在 Vibe Coding 的開發節奏下,功能往往能很快被寫出來。

透過自然語言描述需求,AI 可以在短時間內產出大量程式碼,讓系統迅速成形。

但程式碼產出速度變快,並不代表結構有被好好設計。

相反地,當程式碼是「一段一段被補上來」的時候,類別責任、依賴關係與擴充方式,反而更容易在不知不覺中變得模糊。

這時候,設計原則的功能就不是拿來「要求一開始就設計完美」,而是提供一組用來辨認結構風險的視角。

為什麼系統規模擴大後,問題就開始浮現

不論程式碼是手寫還是由 AI 生成,只要系統開始成長,問題出現的方式其實很一致。

一開始,功能單純、改動不多,結構上的取捨問題不容易立刻產生不良後果。

即使某些設計並不理想,也還能支撐一段時間。

隨著系統規模擴大,功能持續累積,需求反覆調整,參與開發的人也變多,這些早期的設計選擇開始承受壓力,影響也逐漸被放大。

在 Vibe Coding 的情境下,這個過程往往發生得更快,因為修改與新增的頻率更高。

常見的狀況包括:

  • 某些類別越來越大,卻很難說清楚它到底在負責什麼。
  • 每加一個功能,就必須回頭動到既有程式碼。
  • 修改前,需要先確認「這次會不會影響到其他地方」。

這些現象並不是突然發生,而是系統在演進過程中,底層架構與程式碼結構逐漸承受不住變動的結果。

從系統壞味道開始理解設計原則

當這類狀況反覆出現時,往往會被描述成一種「不太對勁」的感覺。

程式還能跑,測試也未必立刻失敗,但修改與理解的成本卻持續升高。

這些容易被察覺、卻不容易精確描述的現象,常被統稱為「系統壞味道」。

它們不一定代表系統立刻會出錯,而是提醒某些結構正在累積風險。

物件導向設計原則,正是從這些壞味道中被整理出來的。

它們提供了一組語言,幫助把模糊的不適感,轉換成可以被討論與判斷的結構問題。

理解這些壞味道之後,除了事後檢視,在開發前做設計選擇時,這些原則同樣能作為一種參考,用來思考某個結構在未來承受變動時,可能會遇到哪些風險。

例如在劃分類別責任、設計介面邊界、決定依賴方向時,設計原則能協助辨識哪些選擇比較容易調整,哪些選擇一旦成形,後續修改成本會快速上升。

而當程式碼主要由 AI 反覆修改時,結構是否清楚,更會直接影響下一次修改是「加一點就好」,還是「越補越亂」。

設計原則是一種判斷工具

物件導向設計原則,是從過往經驗中反覆出現的壞味道所整理出來的一套參考工具,用來協助我們提前辨識與預判結構上的風險。

在設計尚未定型之前,設計原則提供了一個思考方向,協助做出較能承受變動的選擇。

當系統開始變得難以修改、難以理解,這些原則提供了一個切入點,協助釐清問題的性質,而不是直接給出標準答案。

透過設計原則理解壞味道成型的原因與設計取捨,才能讓系統更加穩定地成長。

SRP 單一功能原則:當一個類別什麼都在做

問題:功能越加越多,卻不敢動既有程式

在系統剛開始設計時,把相關功能放在同一個類別裡,通常看起來很合理。

資料在一起、邏輯也在一起,寫起來順手,也不需要在不同檔案之間來回切換。

但隨著需求累積,這個類別開始承擔越來越多責任。

每次只是多加一個小功能,後來漸漸變成每次修改前,都得先花時間確認這次會不會影響到其他行為。

於是最後往往演變成一種狀況:

不是不知道怎麼改,而是不太敢改。

系統壞味道:一個改動,牽動一整片

當一個類別同時處理太多事情,最直接的後果就是改動範圍失控。

常見的壞味道包括:

  • 為了改一個邏輯,卻需要動到看似無關的程式碼。
  • 修改後,測試要重跑一大串,卻很難確定哪些才是真正相關的影響。
  • 程式碼越來越長,閱讀時需要同時記住太多背景。

這些問題並不一定會讓系統立刻出錯,但會讓每一次修改都變得更慢、更有風險。

就算交給 AI 修改,在有無關的程式碼混在「上下文視窗(Context Window)」時,也會影響產出的建議的準確性。

SRP 在處理什麼問題

單一功能原則(Single Responsibility Principle, SRP)關心的核心問題,其實很單純:

一個類別,承受了多少種變動原因。

當不同類型的需求變動,被集中在同一個類別裡,這些變動就會互相干擾。

SRP 的用意,是讓不同原因造成的改動,有機會被分開承擔,而不是全部擠在同一個地方。

這不是要求類別一定要很小,而是希望它的責任邊界清楚,讓改動能有明確的落點。

小例子:拆分前後,改動範圍的差異

假設一個類別同時負責資料處理、商業規則判斷,還順便處理輸出格式。

一開始改起來很方便,但當輸出需求改變時,卻不得不動到商業邏輯所在的程式碼。

當這些責任被拆開後,輸出格式的調整,就只會影響對應的部分。商業規則沒有被動到,相關測試也不需要全部重跑。

審核 Vibe Coding 產出結果時,對於會被改動的區域也比較好預測,而不是只能放棄檢視。

差別不在於結構變得多漂亮,而是每一次改動,影響範圍變得比較可預期

classDiagram
  direction LR

  %% Before SRP
  namespace Before {
    class OrderProcessor1 {
      +processData()
      -calculateBusinessRules()
      -formatOutput()
    }
  }

  %% After SRP
  namespace After {
    class OrderProcessor2 {
      +processData()
    }

    class BusinessRuleService {
      +calculateBusinessRules()
    }

    class OutputFormatter {
      +formatOutput()
    }
  }

  OrderProcessor2 --> BusinessRuleService : uses
  OrderProcessor2 --> OutputFormatter : uses

讓改動只影響該影響的地方

SRP 想解決的,不是「類別要多小」,而是「改動要多集中」。

當發現一個類別因為不同原因被反覆修改,這通常就是單一職責需要被重新檢視的訊號。

讓改動只影響該影響的地方,後續的設計討論,才有空間繼續往前。

ISP 介面隔離原則:當介面開始成為負擔

問題:為什麼每次實作介面都很痛苦

在系統成長的過程中,介面(interface)通常是為了「先對齊用法」而出現的。

一開始把相關操作放進同一個介面,看起來很直覺,也能快速讓實作類別對齊行為。

但隨著需求增加,這個介面開始被不同情境共用。

有些實作類別只需要其中一小部分功能,卻被迫實作整個介面。有些方法對某些實作者來說,甚至沒有合理的意義。

久而久之,實作介面不再是對齊行為,而是一種負擔。

系統壞味道:修改一個行為,卻影響所有實作者

當介面變得過大,最明顯的壞味道會出現在修改時。

只要介面新增或調整一個方法,所有實作這個介面的類別都必須跟著修改。

即使多數實作者根本用不到這個行為,也無法置身事外。

結果是:

  • 介面一動,實作者全面受影響。
  • 實作中出現空方法、拋未實作例外、或「先放著不管」的暫時性處理。
  • 介面越來越難調整,反而變成系統中最不敢碰的地方。

這些現象通常代表,介面已經失去了原本作為「行為契約」的角色。

ISP 在處理什麼問題

介面隔離原則(Interface Segregation Principle, ISP)關心的是誰真正需要哪些行為

ISP 的核心概念很單純:

不應該強迫實作者依賴它們用不到的方法。

當不同使用情境被塞進同一個介面,這些情境之間的變動就會互相牽連。

ISP 希望透過拆分介面,讓依賴關係更貼近實際使用方式,而不是為了方便管理而集中。

這不是要求介面越小越好,而是讓每個介面都有清楚、一致的使用對象。

小例子:把一個大介面拆成多個小介面

假設一個介面同時定義了查詢、更新與報表輸出行為。

有些實作只負責查詢,卻必須實作更新與輸出相關的方法。

當這些行為被拆分成不同介面後,查詢相關的實作只需要依賴查詢介面。

報表輸出的調整,不會影響到只負責查詢的類別。

差異不在於介面數量變多,而在於每個實作者只承擔它真正需要的依賴

classDiagram
  direction LR
  %% Before ISP
  namespace Before {
    class IDataService {
      +queryData()
      +updateData()
      +generateReport()
    }

    class QueryOnlyService {
      +queryData()
      +updateData()
      +generateReport()
    }
  }

  %% After ISP
  namespace After {
    class IQueryService {
      
      +queryData()
    }

    class IUpdateService {
      
      +updateData()
    }

    class IReportService {
      
      +generateReport()
    }

    class QueryOnlyService2 {
      +queryData()
    }
  }

  QueryOnlyService ..|> IDataService
  QueryOnlyService2 ..|> IQueryService

介面應該貼近使用方式

ISP 關心的不是介面設計得多完整,而是依賴關係是否合理。

當發現某些實作者被迫實作用不到的方法,或介面一改就影響過多實作,這通常就是介面需要被重新切分的訊號。

讓介面貼近實際使用方式,系統在調整時,才不會被不必要的依賴拖住。

OCP 開閉原則:需求一來就必須改舊程式

問題:新增功能,卻一定要動到核心邏輯

在系統初期,需求單純,直接在原有程式碼中加入判斷或流程,通常是最快的做法。功能可以很快完成,也不需要先想太多結構上的安排。

但隨著需求增加,這樣的做法開始出現後果。每次要新增一種行為,看起來只是多一個條件,實際上卻必須動到已經穩定運作的核心邏輯。

於是出現一個常見狀況:

需求本身不複雜,修改卻讓人猶豫。

因為一旦動到核心程式,就很難保證不會影響既有功能。

系統壞味道:舊程式越來越脆弱

當新增需求只能透過修改既有程式來完成,系統會逐漸累積一種脆弱性。

條件判斷越來越多,流程分支越來越複雜。

為了支援新行為,舊程式必須反覆被打開修改。

測試覆蓋不足的地方,風險會隨著每一次修改放大。

久而久之,核心程式碼成為系統中最不敢碰的區域。

功能明明還在擴充,結構卻在退化。

OCP 在處理什麼問題

開閉原則(Open-Closed Principle, OCP)關心的,是系統面對需求變動時的承受方式。

它提出的方向是:

對擴充保持開放,對修改保持封閉。

這裡的「封閉」,不是完全不能改,而是希望既有、已經被驗證過的邏輯,不需要因為每一個新需求而反覆被修改。

新的行為應該透過擴充的方式加入,而不是不斷侵入原本的結構。

OCP 的重點不在於避免所有修改,而在於控制修改發生的位置。

小例子:新增行為時,改動位置的差異

假設系統中有一段邏輯,需要根據不同類型,走不同的處理流程。

在一開始,最直覺的做法通常是用條件判斷,把所有情況集中在同一個地方處理。

這樣的設計在需求不多時運作良好,但每新增一種類型,就必須回頭修改這段核心判斷。

流程本身沒有變複雜,修改風險卻會隨著每一次需求增加而累積。

public enum PaymentType
{
    CreditCard,
    BankTransfer,
    Wallet
}

// 用 if / else 的做法
public class PaymentService
{
    public void Pay(PaymentType type, decimal amount)
    {
        if (type == PaymentType.CreditCard)
        {
            // 信用卡付款流程
            ChargeCreditCard(amount);
        }
        else if (type == PaymentType.BankTransfer)
        {
            // 轉帳付款流程
            ProcessBankTransfer(amount);
        }
        else if (type == PaymentType.Wallet)
        {
            // 錢包付款流程
            DeductWallet(amount);
        }
    }

    private void ChargeCreditCard(decimal amount)
    {
        // ...
    }

    private void ProcessBankTransfer(decimal amount)
    {
        // ...
    }

    private void DeductWallet(decimal amount)
    {
        // ...
    }
}

所謂「可擴充的設計」,差別就在這裡,它會先把穩定的流程骨架和容易變動的行為細節分開。

核心流程只負責一件事:

根據條件找到對應的處理方式,然後執行它。

至於實際怎麼處理,則交給各自獨立的實作。

當結構調整成這樣之後,新增一種行為,不再需要修改原本的流程判斷。

只要新增一個對應的實作,並讓系統能夠識別它,原本的程式碼就可以保持不動。

差異不在於寫法是否比較高級,而在於需求出現時,改動是集中在「新增的地方」,還是被迫回頭侵入「已經穩定的核心程式」


// 先定義一個穩定的抽象行為
public interface IPaymentHandler
{
    void Pay(decimal amount);
}

// 接著,每一種付款方式各自實作
public class CreditCardPaymentHandler : IPaymentHandler
{
    public void Pay(decimal amount)
    {
        // 信用卡付款流程
    }
}

public class BankTransferPaymentHandler : IPaymentHandler
{
    public void Pay(decimal amount)
    {
        // 轉帳付款流程
    }
}

public class WalletPaymentHandler : IPaymentHandler
{
    public void Pay(decimal amount)
    {
        // 錢包付款流程
    }
}

// PaymentService 只保留穩定流程骨架
public class PaymentService
{
    private readonly Dictionary<string, IPaymentHandler> _handlers;

    public PaymentService(Dictionary<string, IPaymentHandler> handlers)
    {
        _handlers = handlers;
    }

    public void Pay(string paymentType, decimal amount)
    {
        if (!_handlers.TryGetValue(paymentType, out var handler))
        {
            throw new InvalidOperationException("Unsupported payment type");
        }

        handler.Pay(amount);
    }
}

擴充應該是加東西,而不是改東西

OCP 想解決的,是需求增加時,系統結構能不能承受變動。

當發現每新增一個功能,就必須修改既有核心程式,這通常代表擴充點尚未被妥善處理。

讓新行為透過擴充加入,而不是反覆修改舊程式,系統在成長過程中,才不會越來越脆弱。

LSP 里氏替換原則:繼承之後卻不能替換

問題:子類別一加入,系統行為就開始變怪

在物件導向設計中,繼承常被用來重用既有行為。

子類別看起來是父類別的一種,自然也就被期待能在同樣的地方被使用。

但在實務上,問題往往不是出現在繼承本身,而是出現在使用繼承之後,行為卻不再一致

原本可以安心呼叫的流程,開始需要額外判斷。某些子類別加入後,系統行為出現例外或邏輯偏差。

呼叫端不得不開始區分「現在拿到的是哪一個子類別」。

這些現象通常不是語法錯誤,而是設計假設被悄悄破壞。

系統壞味道:為了分辨子類別,判斷邏輯不斷增加

當子類別無法真正替換父類別時,最明顯的壞味道會出現在呼叫端。

原本只需要面對一種抽象型別,現在卻必須加上條件判斷。

為了避開某些子類別的特殊行為,程式開始出現例外處理或防禦性判斷。

結果是:

  • 呼叫端變得越來越複雜。
  • 子類別之間的差異被外洩到使用它們的地方。
  • 系統對繼承關係的信任度逐漸降低。

一旦走到這一步,繼承帶來的重用價值就幾乎消失了。

LSP 在處理什麼問題

里氏替換原則(Liskov Substitution Principle, LSP)關心的核心問題是:

子類別是否真的能在不改變系統行為的前提下,替換父類別。

這裡的「替換」,不是只看型別是否相容,而是看行為是否符合原本的預期。

如果某個子類別在使用時,需要額外說明「這個不能這樣用」、「那個情況要特別注意」,那麼這個繼承關係本身就值得被懷疑。

LSP 的用意,是讓繼承關係成為一種可靠的承諾,而不是潛在的風險。

小例子:看似合理的繼承,卻破壞了原本假設

假設系統中有一個 Rectangle 類別,提供設定寬高並計算面積的能力,這個類別被多處使用。


public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int Area()
    {
        return Width * Height;
    }
}

在使用端,邏輯很單純合理:


public void ResizeAndPrintArea(Rectangle rect)
{
    rect.Width = 5;
    rect.Height = 4;

    Console.WriteLine(rect.Area()); // 預期輸出 20
}

在這個前提下,任何 Rectangle 的子類別,都應該能被安全地傳進來使用。

接著需求變化出現了,從概念上來看,正方形「是一種」長方形,因此很自然地選擇用繼承:


public class Square : Rectangle
{
    public override int Width
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
    }

    public override int Height
    {
        set
        {
            base.Width = value;
            base.Height = value;
        }
    }
}

從型別角度來看,這段程式碼完全合法。Square 確實是 Rectangle,也能被當成 Rectangle 傳遞。

這時如果將 Square 傳入之前寫的 ResizeAndPrintArea,程式編譯不會有問題,也可以執行,結果卻不是預期的 20,而是 16。

原因不在語法,而在行為假設被破壞,呼叫端假設 WidthHeight 可以獨立設定,但 Square 卻偷偷改變了這個行為。

於是,原本「只要是 Rectangle 都能這樣用」的假設,不再成立。

接下來常見的抄捷徑做法是:


public void ResizeAndPrintArea(Rectangle rect)
{
    if (rect is Square)
    {
        // 特殊處理
    }

    rect.Width = 5;
    rect.Height = 4;
}

或是加上程式註解:「如果傳入是 Square,行為會不一樣」。

這些做法,正是 LSP 被破壞後的典型補救行為。

較合理的調整方向是:

  • 移除 Rectangle / Square 之間的繼承關係,改用「組合」關係。
  • 改用更抽象、行為一致的介面。
  • 將「設定寬高」這類行為重新設計,避免破壞原本假設。

重點在於:

如果子類別會改變父類別的基本行為假設,那麼繼承關係本身就值得被重新檢視。

繼承成立的前提,是行為能被信任

LSP 關心的不是「能不能繼承」,而是「該不該繼承」。

當子類別無法完全遵守父類別的行為契約,繼承關係反而會讓系統變得更脆弱。

讓子類別能在任何使用父類別的地方被安心替換,繼承才會成為一種可靠的設計選擇,而不是隱藏問題的來源。

LSP 的出現往往是因為過度依賴「類別繼承」,如果可以將「繼承」改用「組合」關係,LSP 的風險亦會自然降低。

DIP 依賴反轉原則:高層邏輯被實作細節綁死

問題:測試、替換、重構都變得很困難

在不少系統裡,高層模組往往直接使用具體實作。

資料要存,就直接 new 一個 Repository。要送通知,就直接呼叫某個 EmailService。

在需求單純時,這樣的寫法直覺又快速,但隨著系統演進,這些直接依賴開始變成負擔。

測試時,很難只測核心邏輯,因為一定會連動到外部系統。需求一改,底層實作需要替換,高層程式也跟著被迫修改。重構時,牽一髮動全身,修改範圍難以控制。

問題通常不在功能本身,而在依賴的方向。

系統壞味道:高層模組直接依賴低層實作

當高層模組直接依賴具體實作時,結構會出現一個明顯的壞味道。

高層的業務邏輯,開始知道太多細節。例如資料是怎麼存的、通知是怎麼送的、外部服務怎麼呼叫。

結果就是:

  • 底層實作一變,高層邏輯就必須跟著改。
  • 測試核心邏輯時,總是被外部依賴拖住。
  • 系統的彈性,被最底層的選擇鎖死。

這類問題通常不是某一行程式寫錯,而是整體依賴方向出了問題。

DIP 在處理什麼問題

依賴反轉原則(Dependency Inversion Principle, DIP)關心的是:

高層與低層模組之間,誰應該依賴誰。

DIP 提出的核心方向有兩個重點:

  • 高層模組不應該依賴低層模組,兩者都應該依賴抽象。
  • 抽象不應該依賴細節,細節應該依賴抽象。

這裡的「反轉」,指的不是程式執行流程,而是依賴關係的方向。

透過抽象把高層邏輯與低層實作隔開,高層應關心的是「要做什麼」,而不是「怎麼做」。

小例子:把依賴從實作移到抽象

假設一個訂單流程,完成後需要儲存資料並發送通知。

如果高層流程直接依賴資料庫與郵件實作,測試與替換都會變得困難。

當結構改為依賴抽象後,高層流程只知道「有一個儲存行為」與「有一個通知行為」。

實際用的是資料庫、記憶體、Email 還是其他方式,則由底層實作決定。

這樣一來:

  • 核心流程不需要因為實作替換而修改。
  • 測試時,可以用測試替身實作,專注驗證業務邏輯。
  • 底層技術選擇的變動,不再直接影響高層設計。

差異不在於多寫了介面,而在於高層邏輯不再被細節綁住

依賴方向,決定系統的彈性

DIP 關心的重點是「依賴關係是否合理」。

當高層邏輯直接依賴低層實作,系統的彈性往往會被過早鎖死,AI 也很難為生成有效的單元測試(Unit Tests)。

透過 DIP,才有辧法要求 AI 「針對介面模擬(Mock)出一個實作並撰寫測試」,這能大幅提升 Vibe Coding 開發的安全感。

讓高層與低層透過抽象對齊,各自關注自己的責任,系統在測試、重構與演進時,才有真正調整的空間。

REP 重用/發佈等價原則:為什麼一改就要整包重發

問題:只改一小塊,卻影響整個套件

在模組或套件被切分之後,理論上修改影響範圍應該變小。

但實務上常見的情況卻是明明只改了一個小功能,但發佈時還是不得不更新整個套件。

這種情況一開始不一定造成困擾。版本還少、相依關係不多,重發一次看起來也不麻煩。

但隨著使用端增加、版本數變多,每一次發佈都開始變得小心翼翼。原本只是局部調整,卻被迫承擔整包變更的風險。

系統壞味道:版本更新與風險不成比例

當發佈單位與實際改動範圍不一致時,會出現幾個明顯的壞味道。

只要套件裡任何一個類別被修改,就必須整包重新發佈。使用者即使只需要其中一小段功能,也被迫跟著升級所有內容。

所以團隊會開始避免調整,因為「一動就要重發」。久而久之,套件變成一個「不能輕易動」的巨石。

問題不在於發佈流程,而在於發佈邊界的劃分方式。

REP 在處理什麼問題

重用/發佈等價原則(Reuse / Release Equivalence Principle, REP)關心的是:

哪些東西應該被一起發佈。

REP 的核心概念很直接:

  • 會一起重用/發佈的類別,應該被放在同一個套件中。
  • 不會一起重用/發佈的類別,就不該被強迫綁在同一個發佈單位。

換句話說,套件不只是程式碼的集合,而是一個重用/發佈與版本管理的單位。

如果套件內的類別有不同的發佈節奏,那這個套件的邊界就值得重新檢視。

小例子:重新調整套件邊界後的影響

假設一個套件同時包含核心商業邏輯與多種輔助功能。核心邏輯變動頻率低,輔助功能卻經常調整。

在這種結構下,每次調整輔助功能,都必須重新發佈整個套件。即使核心邏輯完全沒有變,也必須承擔版本更新的成本。

當這些輔助功能被拆成獨立套件後,情況就不同了。輔助功能可以依自己的節奏發佈,核心套件則維持穩定。

差異不在於套件的多寡,而在於發佈風險是否被限制在實際改動的範圍內

一起發佈的東西,應該有共同理由

REP 想解決的,不是套件要不要拆,而是拆的依據是什麼。

如果一個套件內的內容,經常因為不同原因被修改,那發佈邊界就可能不合理。

讓發佈單位與實際變動對齊,系統在版本演進與維護上,才不會被不必要的風險拖慢。

CCP 共同封閉原則:常一起改的卻分散在各處

問題:一個需求改動,卻要動到很多地方

在需求開始變動之後,最讓人困擾的情況之一,是改動範圍難以掌握。

看起來只是調整一個規則,實作時卻必須修改多個類別,甚至橫跨不同模組。

這種改動方式不只花時間,也容易出錯。

因為每一次修改,都需要確認是否有某個地方被遺漏。

久而久之,團隊對修改產生抗拒,寧願繞路,也不想正面調整結構。

系統壞味道:變動沒有集中點

當相關的改動分散在各處,系統會出現一個明顯的壞味道。

  • 同一類需求變更,需要在多個位置重複處理。
  • 修改的地方越來越多,理解與測試成本同步上升。
  • 只要漏改一處,行為就會不一致。

這通常代表,系統的結構沒有反映實際的變動模式。

CCP 在處理什麼問題

共同封閉原則(Common Closure Principle, CCP)關心的是:

會因為同一個原因一起被修改的類別,是否被放在同一個模組中。

CCP 的想法是,把「一起變動」當成模組劃分的重要依據。

如果某些類別經常因為相同的需求調整而被修改,那它們就應該被集中在同一個邊界內。

這裡的「封閉」,指的是模組對外的穩定性。

當需求變動發生時,希望修改能被封閉在模組內,而不是擴散到整個系統。

小例子:把常一起變動的類別聚在同一個模組

假設系統中有一組與價格計算相關的規則。

折扣、稅率、促銷邏輯分散在不同類別中,看起來職責清楚,卻經常需要一起調整。

每次活動規則變更,都得逐一修改這些類別。

改動雖然正確,但風險與負擔隨之增加。

當這些價格相關的邏輯被集中到同一個模組後,需求變更只需要在這個模組內處理,其他部分則保持不動。

差異不在於類別的數量,而在於變動是否能被關在同一個邊界裡

變動應該被包在同一個邊界裡

CCP 強調的是修改行為是否集中在一個邊界裡。

當發現某些類別總是一起被修改,卻散落在不同地方,這通常就是模組邊界需要調整的訊號。

讓會一起變動的東西待在一起,系統在演進時,才不會因為零散的改動而失控。

CRP 共同重用原則:不該依賴的也被拉進來

問題:只用一小部分,卻被迫依賴全部

在模組被切分之後,重用本來應該讓事情變得更簡單。

引入一個套件,只是為了使用其中一個功能,理論上不應該承擔其他無關的負擔。

但實務上常見的情況是:

  • 明明只需要一個類別,卻被迫依賴整個模組。
  • 模組裡的其他類別,成了無法避免的附帶品。

這種重用方式,表面上節省了重寫成本,實際上卻引入了更多不必要的風險。

系統壞味道:模組被過度耦合

當模組被「整包依賴」,系統會開始出現過度耦合的壞味道。

模組內任何一個變動,都可能影響所有使用者。使用端被迫關心自己其實用不到的內容。版本升級時,常因為不相關的改動而產生衝突。

結果是,重用帶來的不是彈性,而是額外的負擔。

CRP 在處理什麼問題

共同重用原則(Common Reuse Principle, CRP)關心的是哪些類別應該被一起依賴

CRP 的核心概念是:

如果使用者不需要模組中的所有類別,那它就不應該被迫依賴整個模組。

換句話說,模組的邊界應該反映實際的重用關係。會一起被使用的類別,才適合放在同一個模組中。

這裡談的不是模組大小,而是依賴的精準度。

小例子:拆分模組後的依賴差異

假設有一個工具模組,裡面同時包含字串處理、日期轉換與檔案存取功能。

某個系統只需要字串處理,卻被迫引入整個模組。

當檔案存取功能調整或出現問題時,即使與字串處理無關,使用端也必須面對版本升級與相依調整。

當這些功能被拆成更聚焦的模組後,使用端只需要依賴實際用到的部分,其餘變動不再造成影響。

差異不在於模組變多,而在於依賴是否真的反映了使用關係

重用不該帶來額外負擔

CRP 強調的是重用的代價。

當依賴一個模組,卻被迫承擔大量無關內容時,重用本身就需要被重新檢視。

只讓會一起被使用的類別待在一起,依賴才不會成為系統演進的拖累。

ADP、SDP、SAP:當依賴關係開始失控

問題:系統卡住,誰也不敢先動

當系統規模擴大、模組數量增加,依賴關係會逐漸變得複雜。

一開始只是單向依賴,久了之後,卻發現模組之間開始互相牽制。

這時候常出現一種狀況:「明明知道某個地方需要調整,卻沒有人敢先動。因為一旦修改,可能會連動到其他模組,甚至影響整個系統。」

不是功能做不出來,而是結構讓人無從下手。

系統壞味道:循環依賴與錯誤的依賴方向

當依賴關係開始失控,最明顯的壞味道通常表現在兩個面向。

  1. 循環依賴。
    模組 A 依賴模組 B,模組 B 又反過來依賴模組 A。這種結構讓任何一方都無法獨立演進,修改成本急遽上升。
  2. 依賴方向錯亂。
    理論上應該穩定的模組,反而經常被修改。變動頻繁的模組,卻成了許多地方的依賴對象。

這些狀況往往同時存在,讓系統逐漸失去調整空間。

ADP 在處理什麼問題

無循環依賴原則(Acyclic Dependencies Principle, ADP)關心的是依賴關係中是否存在循環

ADP 的要求很明確:

模組之間的依賴關係,應該形成一張沒有循環的有向圖。

一旦出現循環,模組就不再是獨立單位。任何一個小改動,都可能迫使整個循環一起被修改與測試。

打破循環,通常意味著重新思考責任邊界或引入新的抽象。

SDP 在處理什麼問題

穩定依賴原則(Stable Dependencies Principle, SDP)關心的是依賴方向是否指向更穩定的模組

所謂穩定,指的是變動頻率低、使用端多的模組。這類模組一旦被依賴,就很難隨意修改。

SDP 提出的方向是:

變動頻繁的模組,應該依賴穩定的模組。

如果一個經常變動的模組成為他人依賴的對象,系統整體的變動成本就會被放大。

SAP 在處理什麼問題

穩定抽象原則(Stable Abstractions Principle, SAP)關心的是穩定的模組,是否同時具備足夠的抽象程度

如果一個模組很穩定,又被大量依賴,但內部卻充滿具體實作,那任何調整都會變得困難。

SAP 的核心觀點是:

穩定的模組應該偏向抽象,讓未來的變動可以透過擴充,而不是修改既有實作來完成。

小例子:調整依賴方向後,系統行為的差異

假設某個核心模組直接依賴一個經常變動的實作模組。隨著需求調整,核心模組便會不得不跟著反覆修改。

當依賴方向被調整後,讓核心模組只依賴穩定的抽象介面,實作模組則負責依附這些抽象。

依賴方向被重新整理之後,原本彼此糾纏的循環關係得以解除,模組之間開始能夠各自演進。

實際的變動會自然集中在那些本來就不穩定、經常調整的模組中。而被大量依賴的核心模組則可以維持相對穩定,持續被重用,不必因為每次需求變化就被迫修改。

依賴關係,決定系統能不能長大

ADP、SDP 與 SAP 關注的是同一件事:

依賴關係是否讓模組能夠獨立演進。

當循環依賴存在、依賴方向錯亂、穩定模組缺乏抽象,系統就會逐漸陷入「誰也不敢動」的狀態。

讓依賴關係清楚、有方向,系統才有空間繼續成長,而不只是勉強維持。

新手常見誤解:為什麼照設計原則做,系統還是很亂

在實務中,設計原則本身很少真的造成問題,真正讓系統變得更複雜的,往往是對原則的理解方式。

以下幾個誤解,在新手階段特別常見,也最容易讓設計討論走偏。

誤解 1:物件導向設計原則是一張檢查表

不少人在學完一輪設計原則後,會下意識把它們當成一張檢查清單。

SRP 有沒有做到、OCP 有沒有符合、依賴是不是反轉了,設計討論變成逐條對照。

問題在於,設計原則並不是要求「全部同時成立」。

它們是在描述不同結構壓力下的取捨方向,而不是用來驗證設計正確與否的規則。

當討論只剩下「這有沒有違反某個原則」,反而容易忽略系統真正承受的是哪一種變動壓力。

誤解 2:設計原則只在出問題後才有用

另一個常見想法,是把設計原則完全當成事後檢討工具。

只有當系統已經很難改、很難測,才回頭對照是哪個原則沒顧好。

這樣的理解讓設計原則少了一半價值。

設計原則確實能幫助釐清壞味道,但它們同時也能在設計階段,用來評估某個結構選擇未來可能承受的風險。

差別只在於,設計階段是在預測未來「可能會出現什麼壞味道」,而不是已經發生的問題。

誤解 3:設計原則等同於抽象與介面越多越好

在實作時,新手很容易把設計原則直接對應到技術手段。

談到 OCP 或 DIP,就直覺聯想到介面與抽象層,於是先把結構拉得很開。

結果是,系統在還沒有承受變動壓力之前,就先背上了理解與維護成本。

這類設計看起來很「正確」,卻不一定貼近當下的實際需求。

雖然原則很重要,但也需要警惕「過度設計」的問題,過多的介面與層次仍會增加理解系統的負擔。

在實務中,通常遵循事不過三的原則,即重複出現三次或變動發生第二次時,再來考慮進行設計原則的重構。

誤解 4:只要遵守原則,設計自然會變好

有些人會期待,設計原則能提供一組通用答案。只要照著做,系統品質就會自動提升。

但事實上,設計從來都離不開情境

同一個原則,在不同規模、不同變動頻率、不同團隊條件下,適用的方式本來就不一樣。

原則提供的是判斷方向,而不是替代判斷的結論。

先看問題,再談設計原則

設計原則真正有價值的時刻,往往是在結構開始承受壓力時。

理解哪些地方在累積風險,再回頭對照原則的提醒,討論才會落在實際問題上。

把設計原則當成思考工具,而不是規範清單,它們才不會成為另一種讓系統變複雜的來源。

在 Vibe Coding 時代裡,設計原則還有什麼價值與應用

隨著 AI 輔助開發工具成熟,所謂的「Vibe Coding」逐漸成為許多團隊的日常。

開發者以自然語言描述需求,快速生成程式碼,再透過反覆修改逐步前進,產出速度明顯提升。

在這樣的情境下,一個很自然的疑問也隨之出現:

「如果程式碼產出與後續修改都由 AI 處理,還有必要依照設計原則調整結構嗎?」

Vibe Coding 改變的是產出速度,不是系統演進的本質

Vibe Coding 確實大幅降低了寫程式的門檻,也縮短了從想法到可執行程式的距離。

但它並沒有改變一個基本事實,系統仍然會成長,需求仍然會變動,結構仍然會承受壓力

差別只在於,現在不只是人類在面對這些結構問題,AI 也成為主要的修改者之一。

這意味著,程式碼不再只是給人看的成果,而是成為 AI 持續工作的環境。

Vibe Coding 放大的,不只是速度,還有結構風險

Vibe Coding 的強項在於快速嘗試與快速產出,但它同時也會放大一個原本就存在的風險:

程式碼的生成速度,往往快過結構被理解與消化的速度。

AI 產出的程式碼,就算單看每一段合理的,在累積之後,類別責任也還是會開始模糊、依賴關係逐漸糾纏,讓壞味道不知不覺就堆了起來。

這時候,設計原則的價值反而變得更明顯。

它們不負責寫程式,而是幫助判斷哪些地方已經開始承受過多結構壓力

結構出問題時,AI 會自然走向補洞式調整

在結構已經出現壞味道的情況下,即使後續修改都交給 AI,問題並不會消失。

當類別責任混雜、依賴關係糾纏時,AI 很難判斷「哪裡才是正確的修改點」。

在風險評估不明確的情況下,最安全的選擇往往是局部補洞。

程式碼多加一個條件判斷,流程中插入特例處理,或在既有結構旁邊再堆一層邏輯。

每一次修改看起來都能解決眼前問題,卻讓壞味道持續累積。

結果是,系統雖然能跑,但結構越來越難以預測。

設計原則提供「修改決策的邊界」

在 Vibe Coding 的節奏下,設計原則的角色其實發生了轉變。

它們不再只是幫助工程師設計結構,而是開始影響 AI 在修改時會怎麼做選擇。

當結構符合設計原則時,AI 在修改程式碼時會自然得到幾個清楚的訊號:

  • 類別責任單一、介面貼近使用方式,讓修改容易對應到正確位置。
  • 擴充點與抽象邊界明確時,新增需求傾向以「新增實作」完成,而不是侵入核心流程。
  • 依賴方向合理、模組邊界清楚,修改行為較不容易跨界或製造循環。

這些並不是 AI 變得更聰明,而是問題被切分得更好。

設計原則同時是檢視工具,也是溝通語言

在 Vibe Coding 的實務中,設計原則還提供了一組可用來「描述結構期待」的語言。

當能清楚表達:

  • 這個類別只應該承擔一種變動原因。
  • 這段流程需要對擴充開放,但不希望反覆修改核心邏輯。
  • 高層流程不應該直接依賴具體實作。

這些描述,本質上就是設計原則的語言。

它們能協助在下指令時,讓 AI 更容易朝安全的結構方向生成與修改。

在這個角度下,設計原則不只是事後評估的標準,也成為與 AI 協作時的對齊工具。

從「寫得快」轉向「改得穩」

Vibe Coding 解決的是如何快速產出。

設計原則關心的,始終是另一件事,這些程式碼之後能不能被持續、可靠地修改

當專案只是短期驗證,結構問題可能不明顯。

但只要系統需要持續演進,修改決策的不確定性就會逐漸成為成本。

在這個前提下,依照設計原則調整結構,並不是為了未來手動修改,而是為了讓下一次 AI 修改時,選擇的路徑更可預期

當產出變快,判斷反而更重要

Vibe Coding 改變了寫程式的方式,卻沒有改變軟體演進的本質。

系統仍然會累積壞味道,結構仍然需要被整理。

物件導向設計原則在這個時代的價值,不在於限制速度,而在於為高速修改提供一組可靠的判斷邊界。

當程式碼不只被人修改,也持續被 AI 修改時,結構清楚與否,決定的已經不只是可讀性,而是整個演進路徑是否可控。

結語:物件導向設計原則是一套判斷框架

物件導向設計原則反覆在處理的,其實都是同一類問題:

系統在成長過程中,如何承受變動,而不讓風險無限制擴散。

這些原則不是用來取代設計思考,也不是保證設計「正確」的公式。

它們更像是一套整理經驗後的判斷框架,幫助我們在面對設計選擇時,看清楚背後可能帶來的結構後果。

當系統還小時,許多問題並不明顯。隨著功能增加、需求反覆調整、依賴關係變複雜,結構上的取捨才會被放大檢視。

這也是為什麼設計原則常常是在系統演進一段時間後,才真正被感受到價值。

從 SRP、OCP、LSP,到模組與依賴層級的原則,這些觀點並不是彼此獨立存在。

它們關注的是不同層次的同一件事:

改動能不能被限制在合理的範圍內。

把設計原則放在「判斷框架」的位置來看,比起背誦定義,更接近實務經驗。

當開始感覺某個地方難以修改、難以測試、或越來越不敢碰,這些原則提供了一組語言,讓問題能被更清楚地描述與討論。

設計從來不是一次到位的結果,而是一連串取捨的累積。物件導向設計原則存在的意義,不在於要求完美,而在於讓這些取捨能更有意識地發生。


更多精選文章
物件導向設計原則
當 AI 也在改程式碼:物件導向設計原則的真正價值

物件導向設計原則不是一組必須背熟的規則,而是一套用來理解系統為什麼會越來越難改的判斷框架。從類別責任混雜、擴充只能靠修改舊程式,到依賴關係失控與循環依賴,這些問題往往以壞味道的形式出現。即使在 Vibe Coding 與 AI 持續修改程式碼的時代,設計原則的價值反而更明確,因為它們能降低修改決策的不確定性,讓人與 AI 都更容易在結構清楚的邊界內進行調整,確保系統能持續演進,而不只是勉強維持。

深入了解更多 »
Value Stream
價值流是什麼?從流程到價值,理解工作如何真正產生成果

價值流是一種用來理解工作如何產生價值的視角。與只關心流程是否順暢不同,價值流關注的是需求從出現到交付之間,價值是否持續前進。當價值流被看見,等待、重工與過度投入等浪費就會浮現,也才能透過流量指標與業務結果,判斷改善是否真的有意義。透過「價值流」能理解工作為什麼會卡住,以及如何讓價值真正被交付。

深入了解更多 »
Scrum Complete Guide
Scrum 完整解析:核心概念、角色職責、事件流程與工件全指南(2025)

Scrum 是一套用來處理複雜問題的輕量框架,透過短周期迭代、透明資訊和持續檢視來調整方向。它由四種職責、五個事件和三大工件組成,讓團隊能在變動快速的環境中持續交付可用成果。Scrum 的核心不是流程,而是靠經驗主義、價值觀和固定節奏來降低風險、提升透明度、加快回饋。多團隊也能以同一個 Product Goal 協作,並產生單一整合 Increment。只要團隊願意在每個 Sprint 中反覆學習和調整,就能用更穩定的方式把產品往正確方向推進。

深入了解更多 »