iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

Codetopia 新手日記:設計模式與原則的 30 天學習之旅系列 第 27

Day 27:KISS/DRY/YAGNI/CUPID:城市座右銘,讓「耶誕城」從災難現場變回可愛天堂!

  • 分享至 

  • xImage
  •  

Codetopia 創城記 (27)|KISS/DRY/YAGNI/CUPID:城市座右銘,讓「耶誕城」從災難現場變回可愛天堂!

1) 今日熱點 (故事開場 & 痛點) ⚡️

Codetopia 的年度盛事——「星港歡樂耶誕城」開幕第一晚,理應是充滿笑聲與閃光燈的奇幻仙境。

結果,它成了大型災難片現場。

時間是晚間十點半,幸福局的戰情室 (War Room) 裡,氣氛比窗外的冷氣團還要冰。螢幕上跳動的數據,每一條都是市民的哀嚎。

「回報!親子區動線跟輪椅族逆流撞在一起了!現場指示牌有七個版本,地貼上的 QR code 還過期了!」現場營運協調官 Arlo 的聲音透過無線電傳來,背景是嘈雜的人聲。

「舞台區燈效太刺激,加上拍照點的爆閃,我們接到超過 50 通幼兒受驚跟周邊住戶的投訴!」

「更糟的是,官網寫『主舞台表演十點結束』,粉專小編發『延長到十點半』,現場廣播卻在催促『準備閉園』...市民現在到底該聽誰的?」

螢幕上,人流分析師 Zephyr 指著一張熱區圖,一個區塊紅到發紫:「這裡,AR 集點跟限定濾鏡的攤位,把整個場域的網路頻寬都吃掉了。現在連隔壁餐車的線上取號系統都轉不出頁面!」

砰!城市幸福局局長 Capri|Chief of Urban Happiness,一掌拍在桌上。她不是生氣,而是一種快刀斬亂麻的決斷。

「夠了。」

她掃視全場,眼神銳利如刀。「各位,昨天的過度設計與溝通混亂,是我們的責任。但市民的聖誕節不能被我們毀了。」

「Arlo,」她轉向協調官,「把今晚所有事故票,濃縮成三張單頁戰報,並準備好離線 QR Code 跟志工手牌的緊急 fallback 方案。」

「Zephyr,」她看向分析師,「把『紅到發紫』這種形容詞給我丟掉。我要你立刻定義出可量測的標準化事件,像是 CrowdHeatmap.v1,並且把『安靜帶分貝 P95』和『綠帶連續性』納入監控。明天我要看到數據反轉!」

「所有人聽著,」Capri 的聲音不大,卻讓整個戰情室瞬間安靜。

「明天,我們只做四件事——KISS、DRY、YAGNI、還有,CUPID。」

是的,你沒聽錯。這不是什麼深奧的技術代碼,而是刻在 Codetopia 骨子裡的城市座右銘。

2) 術語卡 🧭

  • KISS (Keep It Simple, Stupid):保留最小必要表達,把複雜留給系統,把簡單還給市民。

  • DRY (Don't Repeat Yourself):建立單一真實來源 (Single Source of Truth, SSOT),所有訊息都從這裡分發,絕不手動複製貼上。

  • YAGNI (You Ain't Gonna Need It):你不會需要它的!果斷刪掉那些「可能有用但當下沒用」的功能,避免畫蛇添足。

  • CUPID (Composable, Uncoupled, Domain-based, Intuitive, Delightful):城市幸福準則。要求我們的設計是可組合的、低耦合的、圍繞領域的、語彙直覺的,而且...最重要的是,要讓人感到可愛

3) 笑中帶淚 (反例/壞味道) 😭

讓我們把時間倒回災難發生的那一刻,看看這些「壞味道」是如何把一個美好的夜晚變成混亂的迷宮:

  • 地貼墓園:在一個十字路口,市民的腳下居然有 7 張不同單位設計的地貼...它們的圖示風格與用詞完全不同,直接導致親子推車為了看懂指示而停在路中央。(「嘿,我只是想找個地方丟尿布,不是想在原地解謎啊!」)

    (以下為語言無關 pseudocode:示例以 JS 表示字典 / 以 Python 表示測試,兩段彼此獨立、只表達設計概念)

    // ❌ 反例:語彙不一致,各自為政
    floor_label  = "直行↗"
    broadcast    = "請走向前"
    web_copy     = "沿綠色動線前往"
    
    // ✅ 正例:使用共享語彙庫 + 契約測試
    import VOCAB from "./vocab_store.js"
    floor_label = t(VOCAB, "GREEN", "zh-TW") // t() 是帶有 fallback 的翻譯輔助函式
    broadcast   = t(VOCAB, "GREEN", "zh-TW")
    web_copy    = t(VOCAB, "GREEN", "zh-TW")
    
  • 三聲道鬼故事:一位迷路的奶奶,同時聽到志工的廣播稿、滑手機看到社群貼文、又抬頭看到大螢幕跑馬燈。三個管道的資訊互相矛盾,讓她徹底迷失。

    // ❌ 反例:各管道各自手打,多頭馬車
    web_banner = "主舞台表演10點結束"
    fb_post    = "延長到10點半"
    stage_mc   = "準備閉園"
    
    // ✅ 正例:SSOT 元件化 + 一鍵分發
    payload = Payload(version=Version(...), components={...})
    SSOT_Center.publish(payload) // Web / FB / MC 同步更新
    
  • 主線任務被支線任務卡死:AR 集點遊戲佔據了 90% 的公共 Wi-Fi 頻寬,導致餐車取號、票務核銷等核心服務全部癱瘓。

    // ❌ 反例:單一路徑共用,公用頻寬被花俏功能吃光
    path("/wifi/public").attach("ticketing_service")
    path("/wifi/public").attach("AR_game_service") // 佔用 90% 頻寬
    
    // ✅ 正例:QoS 分艙保障 + 權限控管與審計
    // 權限檢查
    assert user.role in {"OpsCoordinator", "ChiefOfHappiness"}, "Permission Denied"
    // 執行變更
    qos.reserve("/wifi/operations", "50%")      // 票務/緊急通道保障 50%
    qos.limit("/wifi/public", max_usage="50%")  // 公眾 AR/濾鏡共用剩下頻寬
    // 審計日誌
    audit.log(user=user.id, action="qos.limit", reason="YAGNI#47")
    
  • 感官的無情壓迫:最需要平緩通過的無障礙坡道,恰好被設置在主舞台重低音喇叭和拍照爆閃燈的中間。這不是耶誕城,這是極限挑戰。

4) 王牌出手 (核心觀念) 👑

隔天清晨,Capri 的四軸決策雷厲風行地展開,一場「可愛革命」開始了。

  • KISS 軸線 - 極致簡化:由無障礙體驗設計師 Poppy 主導,全面啟用「三色一語彙」系統,並備妥離線 QR Code志工手牌作為最終 fallback。

  • DRY 軸線 - 單一來源:由內容版控負責人 Nova 主導,建立 SSOT。其核心是一份帶有契約測試多語系 fallback 機制的共享語彙庫。

  • YAGNI 軸線 - 無情斷捨離:由供應鏈窗口 Jasper 主導,依據「YAGNI 清單」策略性地停用非核心功能。所有變更操作都必須通過角色權限檢查並留下審計日誌

  • CUPID 軸線 - 注入可愛:整個團隊的終極目標。開闢「無障礙綠帶」與「安靜帶」,並將生硬的標語換成可愛的提醒。

5) 導播切景 (表格+兩張 Mermaid) 🗺️

導播,鏡頭拉一下!讓我們看看這場救援行動在 Codetopia 的技術架構中是如何對應的。

視角 觀念/模式 在「耶誕城救援」中的說法
微觀 (GoF) Observer / State / Mediator 廣播台 (Observer) 監聽 SSOT;三色動線 (State) 驅動現場行為;調度中心 (Mediator) 整合回報與數據。
中觀 (EIP/EDA) Topic/Queue / Router 建立 events.emergency.v1events.info.v1 兩個事件主題 (Topic),不同優先權走不同佇列;路由器 (Router) 根據遊客身份引導動線。
宏觀 (MAS) DF / ACL 幸福局如黃頁服務 (DF);志工、場控之間則透過代理通訊語言 (ACL) 建立標準協定。

微觀結構 - 訊息發佈 (Class Diagram)

https://ithelp.ithome.com.tw/upload/images/20251011/201785004iPEgV5xGH.png

中觀流程 - 新動線發佈 (Flowchart)

https://ithelp.ithome.com.tw/upload/images/20251011/20178500NYONPktJfr.png

6) 最小實作 (程式碼範例) 🧩

讓我們看看 Nova 團隊升級後的 SRE 版 SSOT 核心,這是一個強固、可運營的 DRY 實踐。

from dataclasses import dataclass, field
from enum import IntEnum
from typing import Dict, List, Tuple
import time
import uuid

# --- 訊息模型與基礎建設 v3 ---

class Priority(IntEnum):
    EMERGENCY = 3; IMPORTANT = 2; INFO = 1

@dataclass(frozen=True)
class Version:
    """安全的版本物件,避免字串比較陷阱"""
    ts_iso: str  # "2025-12-24T21:00:00Z"
    seq: int     # 1, 2, ...
    def as_key(self) -> Tuple[str, int]:
        return (self.ts_iso, self.seq)

@dataclass(frozen=True)
class Payload:
    """標準化、可追蹤、有時效性的訊息酬載"""
    version: Version
    components: Dict[str, str]
    locale: str = "zh-TW"
    valid_ttl_sec: int = 1800
    priority: Priority = Priority.INFO
    id: str = field(default_factory=lambda: uuid.uuid4().hex)
    created_ts: int = field(default_factory=lambda: int(time.time()))

class ChannelAdapter:
    """輸出管道介面,增加了 TTL 檢查"""
    def update(self, payload: Payload) -> Tuple[bool, str]:
        if time.time() > payload.created_ts + payload.valid_ttl_sec:
            return (True, "skipped_expired") # 視為成功但不顯示,避免阻塞
        return self._do_update(payload)

    def _do_update(self, payload: Payload) -> Tuple[bool, str]:
        raise NotImplementedError

# --- Nova 建立的單一真實來源 (SSOT) v3 ---
class SSOTCenter:
    """具備版本控制、冪等性、重試與可觀測性的 SSOT"""
    def __init__(self):
        self._observers: List[ChannelAdapter] = []
        self._last_version_key: Tuple[str, int] | None = None
        self._seen_ids: set[str] = set()
        self._dlq: list[dict] = [] # 死信佇列 (Dead Letter Queue)

    def subscribe(self, obs: ChannelAdapter): self._observers.append(obs)

    def publish(self, payload: Payload):
        # 冪等性檢查:避免重複處理
        if payload.id in self._seen_ids:
            print(f"**SSOT 中心**:忽略重複投遞 ID={payload.id[:8]}...")
            return
        self._seen_ids.add(payload.id)

        # 版本單調遞增檢查
        key = payload.version.as_key()
        if self._last_version_key and key <= self._last_version_key:
            print(f"**SSOT 中心**:拒絕舊版本訊息 ({payload.version.ts_iso}##{payload.version.seq})")
            return
        self._last_version_key = key

        print(f"\n--- SSOT 中心發佈新訊息 (版本: {key}) ---")
        results, errors = [], []
        for obs in self._observers:
            ok, reason = False, "max_retry_exceeded"
            # 帶有退避的重試機制
            for i in range(3):
                ok, reason = obs.update(payload)
                if ok: break
                time.sleep(1 * (i + 1))
            results.append(ok)
            if not ok: errors.append((type(obs).__name__, reason))

        if not all(results):
            self._trigger_fallback(payload, results, errors)

    def _trigger_fallback(self, payload: Payload, results: List[bool], errors: list):
        for ch_name, reason in errors:
            self._dlq.append({"channel": ch_name, "payload_id": payload.id, "reason": reason, "ts": int(time.time())})
        print(f"🚨 **Fallback 觸發**:部分管道發佈失敗,已寫入 {len(errors)} 筆記錄至 DLQ。")
        print("    -> 指令:Arlo 團隊請立即啟動紙本公告與手牌流程!")

7) 何時用 ✅ / 何時不要用 ⛔️

這些座右銘雖然強大,但也不是萬靈丹。

  • 何時用 (When to Use)

    • KISS/YAGNI:時間緊、壓力大、需要快速上線或修復問題的場景。

    • DRY/SSOT:一份資訊或邏輯需在多處保持絕對一致時。

    • CUPID:設計需要直接面對多元化使用者的產品或服務時。

  • 何時不要用 (When NOT to Use) ⛔️

    • 過度的 YAGNI:在需要前瞻性佈局的長期架構設計中,過度犧牲擴展性。

    • 過度的 DRY:硬把業務領域不同但看似相似的程式碼抽成共用模組,會製造可怕的耦合。這就是「AHA (Avoid Hasty Abstractions)」原則。

    • 不計成本的 CUPID:在純後端內部系統,過度追求 UI/UX 的「可愛」。

8) 鄉民出題 (動手+反模式紅旗) 🚩

現在,輪到你來當英雄了!

反模式紅旗 (Red Flags)

  • 🚩 多頭馬車:團隊裡超過兩人手動複製貼上同一份公告。→ 立刻建立 SSOT

  • 🚩 語彙分裂:UI 按鈕叫「提交」,API 文件叫「創建」,資料庫欄位卻是 is_new。→ 建立共享語彙庫與契約測試

  • 🚩 鍍金功能 (Gold Plating):工程師出於炫技,綁了一堆沒人要的花俏功能。→ 每天對著 YAGNI 清單懺悔

  • 🚩 設計的傲慢:當有人提出無障礙設計建議時,回答卻是「他們不是核心客群」。→ 請把 CUPID 裱框掛在牆上

動手試試看

  1. 你是當時的 Arlo 嗎? 回到「笑中帶淚」的災難現場,用 KISS/YAGNI 原則為活動規劃一份「緊急瘦身計畫」。

  2. 建立你的多語系 fallback 函式:試著擴充下面的 t 函式,讓它在找不到 localedefault_locale 後,能回傳一個帶有警告標誌 ⚠️ 的 key 本身,而不是靜默失敗。

    # 練習:擴充這個函式
    def t(vocab: Dict, key: str, locale: str, default_locale="en"):
        entry = vocab.get(key, {})
        # 當找不到 locale 時,降階到 default_locale,如果再找不到就回傳 key
        return entry.get(locale, {}).get("phrase") or \
               entry.get(default_locale, {}).get("phrase") or \
               key
    
  3. 畫出你的可愛動線:為你的辦公室或學校,設計一套「三色動線語彙」,並想一句體現 CUPID 中「Delightful」精神的可愛提示語。

9) 城市望遠鏡 (進階視野) 🔭

今天我們談的四個座右銘,是將「可愛」這種定性描述,轉化為可量測、可驗收的系統指標的關鍵。當 Capri 要求 Zephyr 提交標準化事件時,她正在做的,就是儀器化 (instrumentation) 我們的城市。

@dataclass(frozen=True)
class CrowdHeatmapV1:
    """人群熱區事件 v1"""
    area_id: str; density_ppm2: float; avg_speed_mps: float; ts_unix: int

@dataclass(frozen=True)
class AccessibilityProbeV1:
    """無障礙設施探測事件 v1"""
    segment_id: str; is_continuous: bool; noise_level_db: float; glare_index: float; ts_unix: int

這兩類事件都以 Topic 名稱 + v1 發佈,例如 events.crowd.heatmap.v1events.accessibility.probe.v1,與前面提到的緊急/一般 Topic 概念完全呼應。它們讓「紅到發紫」或「幼兒受驚」不再是主觀感受,而是可以被監控、告警、並作為迭代改進依據的客觀數據。

10) 結語 & 預告

當晚,耶誕城在四軸策略的力挽狂瀾下恢復了秩序。Zephyr 回報四項核心指標全數達標,營運儀表板上更顯示出漂亮的數據:SSOT 到三管道一致性延遲 P95 僅 2.3 秒,發佈成功率 ≥ 99.5%,重複投遞率 ≤ 0.5%。市民的臉上,重新掛上了笑容。

今日總結:座右銘不只是口號,而是能從混亂中拯救體驗的行動準則。

但 Capri 知道,臨場應變只是第一步。要讓這份「可愛」成為 Codetopia 的常態,他們需要更穩固的架構。

明日預告:當 MVC、分層與六邊形架構在市長辦公室吵成一團,誰能終結這場聖杯戰爭?


附錄:ASCII 版圖示

為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:

微觀結構 - 訊息發佈 (Class Diagram)

┌─────────────────────┐          ┌─────────────────────┐
│    SSOTCenter       │          │      Version        │
├─────────────────────┤          ├─────────────────────┤
│ +publish(payload)   │◇────────▷│ +ts_iso: string     │
│   : void            │          │ +seq: int           │
└─────────────────────┘          │ +as_key(): Tuple   │
           │                     └─────────────────────┘
           │                                │
           │                                │ *
           ▼                                ▼
┌─────────────────────┐          ┌─────────────────────┐
│   ChannelAdapter    │◁ ─ ─ ─ ─ ▷│      Payload        │
│   <<interface>>     │          ├─────────────────────┤
├─────────────────────┤          │ +version: Version   │
│ +update(payload)    │          │ +id: string         │
│   : bool            │          │ +created_ts: int    │
└─────────────────────┘          │ +valid_ttl_sec: int │
                                 └─────────────────────┘

圖例:
◇──▷ 組合關係 (Composition)    ◁─ ─ ─▷ 依賴關係 (Dependency)
*     一對多關係               │      繼承/實作關係

中觀流程 - 新動線發佈 (Flowchart)

    ┌─────────────────┐
    │ Capri 下達新指令 │
    └─────────┬───────┘
              ▼
    ┌─────────────────┐
    │ Nova 產生帶有    │
    │ Version 的 Payload│
    └─────────┬───────┘
              ▼
    ┌─────────────────┐
    │ SSOTCenter.     │
    │ publish(payload)│
    └─────────┬───────┘
              ▼
    ┌─────────────────┐        ┌─────────────────┐
    │ 版本/冪等檢查    │   否   │ 記錄舊版本/重複 │
    │ 通過?          ├───────▶│ 投遞並忽略      │
    └─────────┬───────┘        └─────────────────┘
              │ 是
              ▼
    ┌─────────────────┐
    │ 分發至不同 Topic │
    └─────┬─────┬─────┘
          │     │
    EMERGENCY   INFO
          │     │
          ▼     ▼
    ┌─────────┐ ┌─────────┐
    │ 緊急佇列 │ │ 一般佇列 │
    └─────┬───┘ └─────┬───┘
          │           │
          ▼           ▼
    ┌─────────────────────────┐
    │ Adapter 處理 (含 TTL 檢查)│
    └─────┬─────────────┬─────┘
          │ 失敗        │ 成功
          ▼             ▼
    ┌─────────────┐ ┌───────────────────┐
    │ 寫入 DLQ +  │ │ ✅ 全場資訊       │
    │ 觸發 Fallback│ │   分級同步        │
    └─────────────┘ └───────────────────┘

KISS/DRY/YAGNI/CUPID 四軸決策架構

                    CUPID (可愛軸線)
                         ▲
                         │
                 ┌───────┼───────┐
                 │       │       │
                 │   🎯 核心     │
                 │   決策中心    │
                 │       │       │
                 └───────┼───────┘
                         │
YAGNI (斷捨離) ◀─────────┼─────────▶ DRY (單一來源)
                         │
                         ▼
                   KISS (極簡)

四軸策略說明:
┌─────────┬────────────────────────────────────────┐
│ KISS    │ 保留最小必要表達,把複雜留給系統        │
│ DRY     │ 建立單一真實來源 (SSOT)               │
│ YAGNI   │ 果斷刪掉「可能有用但當下沒用」的功能   │
│ CUPID   │ 可組合、低耦合、直覺、可愛的設計       │
└─────────┴────────────────────────────────────────┘

耶誕城災難與救援時間軸

災難發生                     救援行動                     恢復正常
    │                           │                           │
    ▼                           ▼                           ▼
22:30 ═══════════════════ 00:00 ═══════════════════ 21:00 ═══▶
    │                           │                           │
    │                           │                           │
    ├─ 🚨 親子區動線撞車         ├─ 🎯 KISS: 三色動線系統    ├─ ✅ P95延遲 2.3秒
    ├─ 🚨 七版本地貼混亂         ├─ 🎯 DRY: SSOT中心建立     ├─ ✅ 成功率 ≥99.5%
    ├─ 🚨 三聲道資訊矛盾         ├─ 🎯 YAGNI: AR功能限流     ├─ ✅ 重複率 ≤0.5%
    └─ 🚨 AR吃光頻寬            └─ 🎯 CUPID: 無障礙綠帶     └─ ✅ 市民重現笑容

圖例:
🚨 問題點    🎯 解決方案    ✅ 成效指標    ═══▶ 時間軸


上一篇
Day 26:城市憲法 II (L/I/D):插頭要能換,契約不能破!
系列文
Codetopia 新手日記:設計模式與原則的 30 天學習之旅27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言