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 骨子裡的城市座右銘。
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):城市幸福準則。要求我們的設計是可組合的、低耦合的、圍繞領域的、語彙直覺的,而且...最重要的是,要讓人感到可愛!
讓我們把時間倒回災難發生的那一刻,看看這些「壞味道」是如何把一個美好的夜晚變成混亂的迷宮:
地貼墓園:在一個十字路口,市民的腳下居然有 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")
感官的無情壓迫:最需要平緩通過的無障礙坡道,恰好被設置在主舞台重低音喇叭和拍照爆閃燈的中間。這不是耶誕城,這是極限挑戰。
隔天清晨,Capri 的四軸決策雷厲風行地展開,一場「可愛革命」開始了。
KISS 軸線 - 極致簡化:由無障礙體驗設計師 Poppy 主導,全面啟用「三色一語彙」系統,並備妥離線 QR Code與志工手牌作為最終 fallback。
DRY 軸線 - 單一來源:由內容版控負責人 Nova 主導,建立 SSOT。其核心是一份帶有契約測試與多語系 fallback 機制的共享語彙庫。
YAGNI 軸線 - 無情斷捨離:由供應鏈窗口 Jasper 主導,依據「YAGNI 清單」策略性地停用非核心功能。所有變更操作都必須通過角色權限檢查並留下審計日誌。
CUPID 軸線 - 注入可愛:整個團隊的終極目標。開闢「無障礙綠帶」與「安靜帶」,並將生硬的標語換成可愛的提醒。
導播,鏡頭拉一下!讓我們看看這場救援行動在 Codetopia 的技術架構中是如何對應的。
視角 | 觀念/模式 | 在「耶誕城救援」中的說法 |
---|---|---|
微觀 (GoF) | Observer / State / Mediator | 廣播台 (Observer) 監聽 SSOT;三色動線 (State) 驅動現場行為;調度中心 (Mediator) 整合回報與數據。 |
中觀 (EIP/EDA) | Topic/Queue / Router | 建立 events.emergency.v1 與 events.info.v1 兩個事件主題 (Topic),不同優先權走不同佇列;路由器 (Router) 根據遊客身份引導動線。 |
宏觀 (MAS) | DF / ACL | 幸福局如黃頁服務 (DF);志工、場控之間則透過代理通訊語言 (ACL) 建立標準協定。 |
微觀結構 - 訊息發佈 (Class Diagram)
中觀流程 - 新動線發佈 (Flowchart)
讓我們看看 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 團隊請立即啟動紙本公告與手牌流程!")
這些座右銘雖然強大,但也不是萬靈丹。
何時用 (When to Use) ✅
KISS/YAGNI:時間緊、壓力大、需要快速上線或修復問題的場景。
DRY/SSOT:一份資訊或邏輯需在多處保持絕對一致時。
CUPID:設計需要直接面對多元化使用者的產品或服務時。
何時不要用 (When NOT to Use) ⛔️
過度的 YAGNI:在需要前瞻性佈局的長期架構設計中,過度犧牲擴展性。
過度的 DRY:硬把業務領域不同但看似相似的程式碼抽成共用模組,會製造可怕的耦合。這就是「AHA (Avoid Hasty Abstractions)」原則。
不計成本的 CUPID:在純後端內部系統,過度追求 UI/UX 的「可愛」。
現在,輪到你來當英雄了!
反模式紅旗 (Red Flags)
🚩 多頭馬車:團隊裡超過兩人手動複製貼上同一份公告。→ 立刻建立 SSOT。
🚩 語彙分裂:UI 按鈕叫「提交」,API 文件叫「創建」,資料庫欄位卻是 is_new
。→ 建立共享語彙庫與契約測試。
🚩 鍍金功能 (Gold Plating):工程師出於炫技,綁了一堆沒人要的花俏功能。→ 每天對著 YAGNI 清單懺悔。
🚩 設計的傲慢:當有人提出無障礙設計建議時,回答卻是「他們不是核心客群」。→ 請把 CUPID 裱框掛在牆上。
動手試試看
你是當時的 Arlo 嗎? 回到「笑中帶淚」的災難現場,用 KISS/YAGNI 原則為活動規劃一份「緊急瘦身計畫」。
建立你的多語系 fallback 函式:試著擴充下面的 t
函式,讓它在找不到 locale
和 default_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
畫出你的可愛動線:為你的辦公室或學校,設計一套「三色動線語彙」,並想一句體現 CUPID 中「Delightful」精神的可愛提示語。
今天我們談的四個座右銘,是將「可愛」這種定性描述,轉化為可量測、可驗收的系統指標的關鍵。當 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.v1
、events.accessibility.probe.v1
,與前面提到的緊急/一般 Topic 概念完全呼應。它們讓「紅到發紫」或「幼兒受驚」不再是主觀感受,而是可以被監控、告警、並作為迭代改進依據的客觀數據。
當晚,耶誕城在四軸策略的力挽狂瀾下恢復了秩序。Zephyr 回報四項核心指標全數達標,營運儀表板上更顯示出漂亮的數據:SSOT 到三管道一致性延遲 P95 僅 2.3 秒,發佈成功率 ≥ 99.5%,重複投遞率 ≤ 0.5%。市民的臉上,重新掛上了笑容。
今日總結:座右銘不只是口號,而是能從混亂中拯救體驗的行動準則。
但 Capri 知道,臨場應變只是第一步。要讓這份「可愛」成為 Codetopia 的常態,他們需要更穩固的架構。
明日預告:當 MVC、分層與六邊形架構在市長辦公室吵成一團,誰能終結這場聖杯戰爭?
為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:
┌─────────────────────┐ ┌─────────────────────┐
│ 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)
* 一對多關係 │ 繼承/實作關係
┌─────────────────┐
│ Capri 下達新指令 │
└─────────┬───────┘
▼
┌─────────────────┐
│ Nova 產生帶有 │
│ Version 的 Payload│
└─────────┬───────┘
▼
┌─────────────────┐
│ SSOTCenter. │
│ publish(payload)│
└─────────┬───────┘
▼
┌─────────────────┐ ┌─────────────────┐
│ 版本/冪等檢查 │ 否 │ 記錄舊版本/重複 │
│ 通過? ├───────▶│ 投遞並忽略 │
└─────────┬───────┘ └─────────────────┘
│ 是
▼
┌─────────────────┐
│ 分發至不同 Topic │
└─────┬─────┬─────┘
│ │
EMERGENCY INFO
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ 緊急佇列 │ │ 一般佇列 │
└─────┬───┘ └─────┬───┘
│ │
▼ ▼
┌─────────────────────────┐
│ Adapter 處理 (含 TTL 檢查)│
└─────┬─────────────┬─────┘
│ 失敗 │ 成功
▼ ▼
┌─────────────┐ ┌───────────────────┐
│ 寫入 DLQ + │ │ ✅ 全場資訊 │
│ 觸發 Fallback│ │ 分級同步 │
└─────────────┘ └───────────────────┘
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: 無障礙綠帶 └─ ✅ 市民重現笑容
圖例:
🚨 問題點 🎯 解決方案 ✅ 成效指標 ═══▶ 時間軸