KISS、YAGNI 與避免過度設計

身為資深 C# 開發者,你大概看過(甚至寫過)那種「為了未來彈性」而長出三層 wrapper、滿是介面與泛型、卻只有一個實作的程式碼。這一課直接回答三個問題:KISS / YAGNI 實際上怎麼做到、怎麼知道自己沒過度設計、以及什麼時候該停手

是什麼?(KISS、YAGNI、DRY 三者關係與張力)

三者的張力在於:DRY 用力過頭會違反 KISS 與 YAGNI。為了消除兩段「剛好長得像」的程式碼,你可能造出一個被迫服務兩種不同情境的抽象,反而更難懂、更難改。

ℹ️一句話分清楚

KISS 管「複雜度」、YAGNI 管「時間點(現在 vs 未來)」、DRY 管「知識的單一來源」。發生衝突時,優先順序通常是 YAGNI > KISS > DRY。

過度設計的徵兆清單

把這份清單當紅旗(red flag)來自我檢查。出現越多項,過度設計的機率越高:

⚠️最危險的一句話

「我們以後一定會需要」是過度設計最常見的開場白。除非「以後」已寫進需求或近期 roadmap,否則它就是猜測。

怎麼知道自己沒過度設計

用三個可操作的準則自我審查:

1. 現在需要 vs 未來可能 問自己:「這段抽象/參數/開關,是為了通過目前的驗收條件,還是為了我想像中的未來?」只為未來服務、現在沒有任何呼叫端,就是 YAGNI 違規。

2. Rule of Three(重複三次才抽象)

兩次重複往往還看不出「真正共通的本質」,太早抽象容易抽錯維度,造出被迫服務多情境的錯誤抽象。

3. 可逆決策(two-way door)vs 不可逆(one-way door)

💡判斷捷徑

大多數日常程式碼決策都是 two-way door。當你猶豫「要不要先設計得更彈性」,先確認這是不是 one-way door——如果不是,選簡單版,把彈性留到真的需要時再加。

停止規則(什麼叫「夠好」)

過度設計的另一面,是不知道何時收手。給自己一個明確的「完成」標準,同時滿足以下四點就停手

  1. 通過驗收條件:滿足當前需求與測試,沒有遺漏的 case。
  2. 可讀:同組另一位開發者不需要你解釋就能看懂。
  3. 可測:能寫出單元測試,依賴可被隔離(這通常是「需不需要介面」的真正判準)。
  4. 能改:未來要改時,改動範圍可控、不會牽一髮動全身。

達標就停。不要因為「再加一層會更漂亮」而繼續——那是為審美而非需求工作。

C# 對比範例

需求:給定一張訂單金額,滿千折一百,算出應付金額。 這個需求現在只用一次、規則固定。

過度設計版(Factory + Strategy + 泛型 Repository,全部為「未來」服務):

csharp
public interface IDiscountStrategy
{
    decimal Apply(decimal amount);
}
 
public class ThousandDiscountStrategy : IDiscountStrategy
{
    public decimal Apply(decimal amount)
        => amount >= 1000 ? amount - 100 : amount;
}
 
public interface IDiscountStrategyFactory
{
    IDiscountStrategy Create(string strategyName);
}
 
public class DiscountStrategyFactory : IDiscountStrategyFactory
{
    public IDiscountStrategy Create(string strategyName) => strategyName switch
    {
        "thousand" => new ThousandDiscountStrategy(),
        _ => throw new NotSupportedException(strategyName)
    };
}
 
public interface IRepository<TEntity, TKey>
{
    TEntity GetById(TKey id);
}
 
public class OrderService
{
    private readonly IDiscountStrategyFactory _factory;
    public OrderService(IDiscountStrategyFactory factory) => _factory = factory;
 
    public decimal CalculatePayable(decimal amount)
        => _factory.Create("thousand").Apply(amount);
}

問題:四個型別、兩個介面、一個工廠、一個用不到的泛型 Repository——只為了一條 if。沒有第二種折扣策略、沒有第二種實體,所有「彈性」都是猜測。

簡單直接版(KISS + YAGNI):

csharp
public static class OrderPricing
{
    public static decimal CalculatePayable(decimal amount)
        => amount >= 1000 ? amount - 100 : amount;
}

一個純函式、好讀、好測(OrderPricing.CalculatePayable(1200) 直接斷言)。當哪天真的出現第三種折扣規則(Rule of Three),再回頭抽出 IDiscountStrategy 也完全來得及——因為這是 two-way door。

決策流程

遇到「重複出現」或「有人喊未來要擴充」時,照這個流程決定要不要抽象:

1

這是現在的需求嗎?

有對應的驗收條件或 roadmap 項目嗎?若純屬猜測,套用 YAGNI,先不做。

2

重複到第三次了嗎?

未滿三次,先忍住、允許複製貼上。滿三次且模式穩定,才考慮抽象。

3

是 one-way door 嗎?

公開 API、DB schema、對外合約屬不可逆,值得謹慎設計;其餘多為可逆,選簡單版即可。

4

抽象後更好讀好測嗎?

若抽象讓認知成本上升、又沒帶來可測性,就是錯誤抽象,退回簡單版。

5

達到停止標準就收手

通過驗收、可讀、可測、能改,四項皆滿足即停止,不再為審美加層。

常見誤區

⚠️別把 YAGNI 當偷懶的藉口

YAGNI 反對的是「為臆測的未來寫程式」,不是反對工程品質。以下都不在 YAGNI 的豁免範圍:

  • 不寫測試:測試是當前需求的一部分,不是未來功能。
  • 不重構:重複到第三次該抽象卻不抽,是技術債,不是 YAGNI。
  • 不處理錯誤/不做安全防護:這些是當前正確性的需求,現在就需要。
  • 不寫必要的日誌與可觀測性:營運需求是現在的需求。

同樣地,DRY 過頭會造成錯誤抽象:把兩段「碰巧相似但本質不同」的程式碼強行合併,會生出一個塞滿 if/flag 的方法,日後兩邊需求各自演化時,這個共用抽象就成了改不動的瓶頸。記住 Sandi Metz 的話:重複遠比錯誤的抽象便宜

實戰補充:KISS 跟 SOLID 衝突嗎?

Q:既然要 KISS、要 YAGNI,那 SOLID(尤其是 DIP、OCP)不就鼓勵我先加介面、先預留擴充點嗎?兩者矛盾嗎?

A:不矛盾,但有先後。SOLID 是當你已經需要某種彈性時,把它做對的指引;KISS / YAGNI 則回答你現在到底需不需要那種彈性

實務順序:先用 KISS / YAGNI 寫出最簡單可行版 → 出現真實的變化壓力或測試需求 → 用 SOLID 引導重構。先 SOLID 後需求,往往就變成過度設計。

理解測驗

🤔 一段程式碼第二次出現相似邏輯,依 Rule of Three 你應該怎麼做?

🤔 下列哪一項最適合「快速做、之後再改」的 two-way door 思維?

🤔 有人主張「為了 YAGNI,這個功能先不寫測試」,這個說法的問題是?

重點整理

💡一句話記住

現在用不到就先別做(YAGNI)、能簡單就別繞(KISS)、重複三次才抽象(Rule of Three)、可逆的決策就快做、達到「夠好」就停手。

概念管的是什麼可操作準則
KISS複雜度/認知成本選最好讀的做法,不是最短的
YAGNI時間點(現在 vs 未來)沒有驗收條件或 roadmap 就先不做
DRY知識的單一來源管知識重複,不是長得像就合併
Rule of Three何時抽象重複第三次、模式穩定才抽
可逆決策該花多少心力設計two-way door 快做;one-way door 謹慎
停止規則何時收手通過驗收+可讀+可測+能改

你可能也想看

SOLID 五原則配 C# 範例技術債(Technical Debt)

按 ← → 鍵切換課程