物件導向設計原則是什麼?先別急著背名詞
物件導向設計原則,指的是在軟體系統演進過程中,逐漸整理出來的一組結構性經驗。
它們關心的不是語法細節,而是當需求反覆變動時,哪些設計選擇會讓系統比較容易調整,哪些選擇會讓風險不斷累積。
在這樣的脈絡下,常會看到 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。
原因不在語法,而在行為假設被破壞,呼叫端假設 Width 與 Height 可以獨立設定,但 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:當依賴關係開始失控
問題:系統卡住,誰也不敢先動
當系統規模擴大、模組數量增加,依賴關係會逐漸變得複雜。
一開始只是單向依賴,久了之後,卻發現模組之間開始互相牽制。
這時候常出現一種狀況:「明明知道某個地方需要調整,卻沒有人敢先動。因為一旦修改,可能會連動到其他模組,甚至影響整個系統。」
不是功能做不出來,而是結構讓人無從下手。
系統壞味道:循環依賴與錯誤的依賴方向
當依賴關係開始失控,最明顯的壞味道通常表現在兩個面向。
- 循環依賴。
模組 A 依賴模組 B,模組 B 又反過來依賴模組 A。這種結構讓任何一方都無法獨立演進,修改成本急遽上升。 - 依賴方向錯亂。
理論上應該穩定的模組,反而經常被修改。變動頻繁的模組,卻成了許多地方的依賴對象。
這些狀況往往同時存在,讓系統逐漸失去調整空間。
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,到模組與依賴層級的原則,這些觀點並不是彼此獨立存在。
它們關注的是不同層次的同一件事:
改動能不能被限制在合理的範圍內。
把設計原則放在「判斷框架」的位置來看,比起背誦定義,更接近實務經驗。
當開始感覺某個地方難以修改、難以測試、或越來越不敢碰,這些原則提供了一組語言,讓問題能被更清楚地描述與討論。
設計從來不是一次到位的結果,而是一連串取捨的累積。物件導向設計原則存在的意義,不在於要求完美,而在於讓這些取捨能更有意識地發生。


