PRINCIPLE 2 / 4

簡潔優先

Simplicity First
用最少的程式碼解決問題,對抗過度工程。
核心句:用最少的程式碼解決問題。不要任何推測性內容。

用最少的程式碼解決問題。不要任何推測性內容。

問問自己:「資深工程師會說這個太複雜了嗎?」如果是,簡化它。

檢驗標準:問問自己:「資深工程師會說這個太複雜了嗎?」如果是,簡化它。
教學解說:LLM 為什麼傾向過度複雜?

LLM 在訓練過程中學到了大量「設計模式」和「最佳實務」——策略模式、依賴注入、觀察者模式等等。當它收到「加入折扣計算」的需求時,會自動聯想到「折扣有多種類型」→「用策略模式」→「建立完整的抽象層」。

問題在於:時機。過早引入複雜度會讓程式碼更難理解、更難測試、更容易出錯。好的程式碼是簡潔解決今日問題的程式碼,不是過早解決明日問題的程式碼。

關鍵心態:把「簡單」當作專業的象徵,而不是偷懶。資深工程師的標誌不是寫出複雜的架構,而是用最簡單的方式解決問題。

Worked Example:計算折扣函式
對應範例 2-1 — AI 過度設計 vs. 簡潔實作
AI 編碼前(❌ 過度抽象)
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
    @abstractmethod
    def calculate(self, amount: float) -> float: pass
class PercentageDiscount(DiscountStrategy):
    def __init__(self, percentage: float): ...
class FixedDiscount(DiscountStrategy):
    def __init__(self, fixed_amount: float): ...
# ... 策略模式 + 設定物件 + 計算器 = 100+ 行
AI 編碼後(✅ 簡潔實作)
def calculate_discount(amount: float, percent: float) -> float:
    """Calculate discount amount. percent should be 0-100."""
    return amount * (percent / 100)

# 使用
discount = calculate_discount(100.0, 10.0)  # 折抵 $10
關鍵差異
  • 之前:100+ 行,需要 30 多行設定才能做一個簡單計算
  • 之後:3 行函式,直接呼叫即可
  • 核心改變:從「解決未來可能的需求」變成「解決今日的問題」
常見錯誤:AI 的過度工程模式
  • 預測性功能:不問就加入快取、驗證、合併、通知等沒人要求的選項
  • 過早抽象:為了「可能的擴展」建立完整的設計模式架構
  • 萬能函式:一個函式加入 10 個參數,每個參數都有預設值
  • 防禦性編程:為不可能發生的情境加入大量錯誤處理
  • 重構衝動:看到「不夠優雅」的程式碼就想重構,即使它運作正常
練習 / 檢核清單
  • 需求範圍:這是我被要求做的嗎?還是我在猜測?
  • 行數檢查:200 行能寫成 50 行嗎?
  • 抽象必要性:這個抽象會被用到第二次嗎?
  • 參數檢查:函式有超過 3 個參數嗎?
  • 資深工程師測試:資深工程師會說這太複雜了嗎?
  • 重構時機:我是在現在需要,還是在猜測未來需要?

對應範例

到互動範例頁看這個原則的 ❌/✅ 對照:

← 上一原則原則 1 · 編碼前思考
進階單元 · Round 1 Deepening
進階 Worked Example:訂閱計費系統改版 — 支援月付與年付
需求:「我們的訂閱目前只有月付,請支援年付。」聽起來很簡單——但這是過度工程的重災區。
❌ 常見的 AI 直覺(為「未來」蓋一座城)
class SubscriptionPlan(ABC):
    @abstractmethod
    def price(self, base: float) -> float: ...
class MonthlyPlan(SubscriptionPlan): ...
class YearlyPlan(SubscriptionPlan): ...
class PlanFactory:
    @staticmethod
    def create(kind: str, config: PlanConfig) -> SubscriptionPlan: ...
class BillingService:
    def __init__(self, plans: dict, prorater, invoicer, taxer): ...

# 100+ 行:抽象類別、工廠、設定物件、折抵引擎……
# 只為了算出「年付 = 月付 × 12 × 0.8」。
✅ 運用原則 2 的決策過程
「支援年付」的最簡詮釋:給定方案與期間,算出價格。

def price(plan: str, base: float) -> float:
    """plan: 'monthly' | 'yearly'. Yearly gives 20% off."""
    if plan == 'yearly':
        return base * 12 * 0.8
    return base

# 使用
monthly = price('monthly', 10.0)   # 10.0
yearly  = price('yearly', 10.0)    # 96.0

決策紀錄(說給使用者聽):
- 沒有建立 Plan 類別:因為目前只有兩種字串,if 就夠。
- 沒有抽象層:因為「第二種付款維度」(例如一次買 3 年)還沒出現。
- 折扣數字寫死在函式裡:因為折扣策略是業務規則,不是可配置基礎設施。
- 什麼時候重構:當第 3 種方案出現、或折扣邏輯超過一行時,再提抽象。
為什麼選這條路徑
  • 因為複雜度是「負債」,不是「資產」:今天寫的每個抽象,明天都要由每個人維護。年付的業務規則只有兩行,卻用 100 行架構包著——負債比本金高 50 倍。
  • 因為可預測的擴展才值得抽象:「支援月付與年付」是有限集合;「訂閱產品以後會百花齊放」是猜測。原則 2 只為後者買單,不為前者付費。
  • 因為簡單版本反而更好改:當商業團隊說「年付改成 8.5 折」,改一個字元 vs. 改三個 class 的建構參數——前者 10 秒,後者要寫測試說明文件。
深入原理:LLM 為什麼「看到折扣就想到策略模式」?

模型的訓練資料大量來自含設計模式的程式碼——開源專案、Stack Overflow、教學文章。這些資料的統計模式是:需求越抽象,模型就越傾向用「專業架構」回應。策略模式、依賴注入、工廠——模型不是「思考後選擇」這些模式,而是它們在訓練資料裡與「商業規則需求」高度共現,所以生成時被優先喚出。這叫統計性過度設計:模型在複製「看起來資深」的形狀,而不是「解決問題所需的形狀」。

200 行的預設值:模型還有「擴充偏誤」——RLHF 評分時,完整的、考慮周到的答案得分更高,於是「加更多參數、更多防禦、更多功能」成為默認行為。對模型而言,寫少是一件需要被特別要求的事。

Token 經濟的誘因:append 比 revise 便宜。模型可以一直「加」下去而不會推翻自己;而「重寫得更短」需要先刪除再重建——那是更高階的生成任務。所以簡潔不是模型的舒適區,是使用者的紀律。

案例:大家以為遵守原則,其實違反了

「簡潔」最容易被誤解成「短」。一個 Agent 收到「計算平均分」的需求,把 12 行清楚的迴圈壓成一行多層嵌套的 lambda——字元少了,但認知負擔暴增:

avg = (lambda s: (lambda t: t/len(s))(sum(s)))(scores)

這不是簡潔,是混淆。原則 2 定義的成功標準是「資深工程師 30 秒內讀懂」與「最少的不必要認知負擔」,不是「最少的字元數」。簡潔是刪掉不必要的部分,不是刪掉必要的可讀性。

診斷式疑難排解:原則 2 常見故障
症狀可能原因解決方案
收到「加入折扣」就建了 5 個 class設計模式與商業需求的統計共現被觸發要求「先寫最小實作,標為 v1」,並禁止在本輪加入抽象
函式有 8 個參數、全帶預設值推測性可配置性——為「可能的呼叫者」留後路砍到需求所需參數;其餘等真的出現第二個呼叫者再加
為「未來擴展」建立 interface/基底類別過早抽象——誤把「可能」當「必然」套用「第二次使用」法則:抽象必須等到第二個真實使用者出現才值得
200 行程式碼,功能其實只要 50 行防禦性編程+重構衝動疊加重寫為最小版本;同時用原則 3 提醒自己別動無關的既有程式碼
一行看不懂的 lambda/三元運算式串把「短」誤當成「簡潔」改用可讀性標準:資深工程師能否在 30 秒內講出它的行為?不能就拆開
進階挑戰題
  1. 最小簽名:需求「加入幣別匯率轉換」。寫出你認為「剛好夠」的最小函式簽名;再寫出模型「很可能過度設計」的版本。比較兩者的差異並說明你的底線。
    提示:想想匯率來源、快取、歷史匯率、四捨五入——哪些是需求的,哪些是模型的。
  2. 第二次使用法則:什麼情境下,你會在「第一次」使用時就建立抽象?說出你的判斷依據。
    提示:關鍵例外是「風險與成本」——例如安全/併發相關的抽象,延後引入的代價遠高於提前。
  3. 雙原則夾擊:使用者要求「把這段 300 行重構成更簡潔的版本」。你發現它其實可以拆成 3 個清楚函式——這同時涉及原則 2(簡潔)與原則 3(精準)。你要怎麼回應,才兩者都不違反?
    提示:使用者明確要求重構時,原則 3 的「不要順帶改」限制被解除了,但只限於「被要求」的範圍。
進階單元 · Round 2 Deepening
專案級端到端 Worked Example:從零開發「API 速率限制」中間件
需求:「幫我們的 API 加上速率限制,防止濫用。」——以四原則完成一個完整 feature。
階段 1 → 原則 1(編碼前思考):釐清需求邊界
先確認:
1. **範圍**:每個端點獨立限制?還是全站共用?
2. **策略**:固定窗口?滑動窗口?令牌桶?
3. **後端**:單機記憶體夠?還是需要 Redis?
4. **回應**:超限回傳什麼?429 + Retry-After?
5. **白名單**:內部服務要豁免嗎?
→ 確認:固定窗口、單機記憶體、全站 1000 req/min。
階段 2 → 原則 2(簡潔優先):最小實作
# 30 行,不超過
from time import time

class RateLimiter:
    def __init__(self, max_requests=1000, window=60):
        self.max_requests = max_requests
        self.window = window
        self.requests = []

    def allow(self):
        now = time()
        self.requests = [t for t in self.requests if now - t < self.window]
        if len(self.requests) < self.max_requests:
            self.requests.append(now)
            return True
        return False

def rate_limit middleware(app, limiter):
    @app.before_request
    def check():
        if not limiter.allow():
            abort(429, headers={'Retry-After': '60'})

# 先不做:Redis 後端、滑動窗口、每個端點獨立限制、設定檔。
# 為什麼不做?因為現在的需求是「全站 1000/min」,上面 30 行就夠了。
階段 3 → 原則 3(精準修改):只動必要檔案
新增:rate_limiter.py(30 行)
修改:app.py(加一行 import + 掛載 before_request)

刻意沒動的:
- app.py 的既有路由格式 — 單引號雙引號不一致?不改。
- config.py 的結構 — 看到可以更優雅?不動。
- tests/ 裡「看起來沒覆蓋到」的舊測試 — 不碰。
階段 4 → 原則 4(目標驅動執行):逐步驗證
計畫與驗證:
1. 實作 RateLimiter 類別
   驗證:寫 3 個單位測試(允許、滿額、過期重置)全綠
2. 掛載中介層
   驗證:curl 發 1001 個請求 → 第 1001 個回傳 429
3. 驗證 Retry-After header
   驗證:429 回應包含 Retry-After: 60
4. 既有測試
   驗證:完整套件全綠,無回歸

完成標準:4 步全部通過 + 手動 curl 驗證。
四原則如何串成一條鏈
  • 原則 1:如果直接動手,可能選了 Redis(過度)或令牌桶(過複雜),都是假設驅動的過度工程。
  • 原則 2:30 行 vs. 可能 200+ 行的「完整速率限制系統」。先把核心跑起來,痛點出現再擴充。
  • 原則 3:只動 2 個檔案,diff 可以在 10 秒內審完。
  • 原則 4:每步有具體的驗證——不是「它能運作了」,而是「curl 第 1001 次回 429」。
效能 × 品質 × 安全深度:簡潔在三面向的實務影響

效能:過度工程直接消耗效能。每一層抽象都是一次間接呼叫;每一個不需要的參數都是一次分支預測失敗;每一個不需要的 class 都是一次記憶體配置。例如:策略模式的 5 個 class 在 hot path 上比一個 if-else 慢 3-5 倍——不是因為策略模式差,而是因為在此情境下它是多餘的。簡潔不只省行數,也省 CPU。

品質:複雜度是品質的頭號殺手。更多行數 = 更多 bug 容身之處;更多抽象 = 更多需要理解的介面;更多參數 = 更多組合需要測試。心理學研究顯示:人類短期記憶上限約 7±2 項——一個 10 參數的函式已經超出大多數人的理解能力。品質不是「功能完整」,而是「能在 30 秒內被理解」。

安全:過度工程引入不必要的攻擊面。例如:為了一個簡單的設定檔加上 YAML 解析 + 動態 import + plugin 機制——YAML 解析器是已知的反序列化漏洞來源(CVE 數以百計),動態 import 可被用來執行任意程式碼。每一行不需要的程式碼都是一個不需要的漏洞入口。安全的最佳實踐不是「加更多防禦」,而是「少寫程式碼」。

安全案例:過度工程引入的反序列化漏洞

Agent 收到「支援可擴充的折扣規則」需求,建立了 YAML 設定檔 + 動態 import 的 plugin 系統。攻擊者上傳了一份含 !!python/object/apply:os.system ['rm -rf /'] 的折扣規則 YAML——伺服器被遠端執行任意命令。如果一開始只用 if/elif 判斷折扣類型,這條攻擊路徑根本不存在。

面向原則 2 · 簡潔優先相關原則差異說明
核心動作 砍掉不必要的程式碼,只留「今天需要的」 原則 3(精準修改) 兩者都反對「多做」:原則 2 反對的是「做多的抽象」(過度設計),原則 3 反對的是「改多的範圍」(順帶重構)。一個管「寫什麼」,一個管「動什麼」。
判斷標準 「資深工程師會說太複雜了嗎?」 原則 1(編碼前思考) 原則 1 在動手前問「是不是做對的事」,原則 2 在動手時問「是不是用最簡單的方式做」。前者管方向,後者管效率。
對抗的偏誤 統計性過度設計——模型複製「看起來資深」的形狀 原則 4(目標驅動) 原則 2 反對「多做」,原則 4 反對「做了不知道對不對」。兩者搭配:用最少的程式碼(2),每一步都可驗證(4)。
重構時機 等到第二個真實使用者出現才值得抽象 原則 3(精準修改) 原則 2 定義「什麼時候值得重構」;原則 3 確保「重構時只動必要的」。兩者共同控制重構的「觸發條件」與「執行邊界」。
Round 2 互動式檢核清單
  • 我寫的每個 class/function 都有明確的「今天的理由」,不是為「以後可能」
  • 我能用一句話解釋每個抽象存在的原因,且這句話包含真實的使用案例
  • 我會檢查函式參數是否超過 3 個,並嘗試砍到最必要的
  • 在安全敏感的場景(序列化、動態載入、plugin),我會優先選擇更簡單的方案
  • 我區分了「字元少」與「認知負擔低」——後者才是簡潔的真正標準
  • 我對照過「資深工程師 30 秒讀懂」的測試:如果不能,就拆開它