Day 04|雙預言家生推對決:介面契約、物件導向四柱與防禦性編程
「有人立過誓,也有人毀過誓。夜色帶得走一句口頭的承諾,帶不走一行寫進帳本的字。」
——《阿帕契開源審計錄》¹ 卷一·契約篇
幕間 女巫終於決定用毒,倒向一個白天發言老是慢半拍的座位。 天亮,法官只報那個座位夜裡沒了人——沒有翻牌,沒有遺言,誰也不知道倒下的是狼還是好人。 「我到底毒對了沒有?」女巫在帷幕後發抖,這個問題她永遠得不到答案。獵人按住她:「別聲張,還有預言家。他昨晚驗的結果,寫下來了。」
城堡大廳的壁爐燃燒著劈啪作響的松木,清晨的鐘聲剛落,法官那冷峻的聲音穿透了厚重的晨霧:「昨夜平安夜,現在進入警長競選階段。」
這是標準九人局(三隻狼人、三位神職、三個平民)中最驚心動魄的局面——經典殘局之六:「雙預言家生推對決」。在屠邊規則的高壓下,手握 1.5 票生殺大權的警徽成了兩大陣營必爭的咽喉要道。全場貫徹嚴格的「絕發規則」,每個人只能在自己的輪次發言,無人得以插話。此時,長桌對面的 2 號玩家猛然舉起手牌起跳:「我是預言家,昨晚驗了 4 號是金水(好人),警徽流先 7 後 9!」
還沒等大家消化這個資訊,坐在我身旁的 5 號玩家冷笑了一聲,毫不猶豫地拍下桌子:「真預言家在此!昨晚查驗 2 號是個大鐵狼!他是在悍跳搶警徽,企圖給隊友發金水做身份!」
雙預言家對跳!
如果是在前幾世,身為 AI 魁儡的我肯定又會手足無措地向模型求救,生成幾段聽起來慷慨激昂、實則毫無邏輯支撐的辯詞。然而,在經歷了無數次被女巫無聲毒殺與白天公投放逐的死循環後,我們 2N1P 團隊清醒地意識到:悍跳狼之所以能用 AI 幻覺在發言中混淆視聽,根源在於系統缺乏不可妥協的「介面契約」與「狀態封裝」。 當任何人都能在發言時隨心所欲地竄改查驗歷史、捏造不存在的狀態遷移時,謊言便能在沒有編譯期防護的荒野中肆意生長。
口頭的判決會蒸發,寫下的紀錄不會
那天散場後,我在廊下想了很久。
女巫那個永遠得不到答案的問題——「我到底毒對了沒有」——之所以無解,是因為那一夜沒有人翻牌、沒有人把結果寫下來。凡是只活在記憶和口頭裡的東西,隔一個晚上就可能對不上,甚至會被人悄悄改掉一筆而你渾然不覺;唯有被鄭重寫進一份誰都動不了的紀錄,那句話才追究得到。
七號那卷從不離身的羊皮紙,好像就是這樣一份東西——她記下的字,我從沒見過哪一輪被改動。而今天這場雙預言家對跳賭的也是同一件事:誰的查驗紀錄,是寫進了不可竄改的契約裡的?
物件導向四本柱:為混亂的世界築起邊界
在軟體工程領域,特別是面對大型開源專案與複雜的分散式架構時,物件導向程式設計(Object-Oriented Programming, OOP)的四大支柱不是教科書上的死背名詞,而是保護系統不被惡意操作或非預期狀態摧毀的護城河:
- 封裝(Encapsulation):將物件的內部狀態(State)隱藏在邊界之內,僅透過受控的公開方法(Public Methods)暴露能力。預言家昨晚查驗的名冊與時間戳記,絕不能是外部可以隨意寫入的公開欄位;一旦狀態失去封裝,冒牌者便能在白天隨時修改查驗目標。
- 抽象(Abstraction):將複雜的現實世界運作抽離為清晰的核心特徵。我們不在乎查驗過程是透過水晶球還是直覺,我們只定義查驗行為必須產生的保證——給予一個目標,回傳一個確定且不可竄改的判定結果。
- 繼承(Inheritance):建立行為規範的階層關聯,實現代碼與特性的複用,但必須嚴格遵守里氏替換原則(Liskov Substitution Principle, LSP)。子類別不能破壞父類別所承諾的任何先決條件。
- 多型(Polymorphism):呼叫端(全場村民)只需要依賴統一的高階介面,執行期則能動態綁定至不同的具體實作。無論眼前站著的是真預言家還是悍跳狼,他們都必須向同一個契約介面證明自己的合法性。
介面契約與防禦性編程(Defensive Programming)
在「源來適你」(OpenSource4You)社群中,「開源大佬」chia7712 經常強調一個關鍵思維:優秀的代碼不只在理想路徑(Happy Path)下能夠運行,更重要的是在面對惡意輸入、併發競爭或異常狀態時,能展現出極致的韌性與拒絕非法的勇氣。
這正是 Bertrand Meyer 所倡導的契約式設計(Design by Contract, DbC)與防禦性編程的核心靈魂:
- 前置條件(Preconditions):方法被呼叫前必須滿足的邊界。例如:夜間查驗的目標必須是存活玩家、查驗輪次必須單調遞增、一晚只能查驗一人。
- 後置條件(Postconditions):方法執行完畢後必須達成的狀態承諾。例如:回傳的查驗結果必須包含時間戳記且不可變更。
- 不變量(Invariants):物件在其生命週期內必須永遠維持的真理。例如:同一個玩家在歷史查驗紀錄中,其身分判定絕對不能前後矛盾。
許多「Vibe Coder」仰賴 AI 寫出來的代碼之所以在生產環境瞬間崩潰,就是因為缺乏防禦性檢驗。AI 傾向於假設所有傳入的參數都是乾淨合法的,當惡意資料或邊界空值(Null)傳入時,系統便發生狀態污染。真正的防禦性編程,秉持著 Fail Fast(快速失敗)的鐵則:在異常發生的第一時間拋出例外,將災難遏止於搖籃之中。
Java 25 實作:SeerInspectionContract 與真假預言家的契約檢驗
在現代 Java 25 體系中,我們善用 record 的不可變性(Immutability)與介面契約,設計一套能夠在執行期徹底檢驗冒牌者的防禦程式碼。以下代碼嚴格遵循 100% 英文命名與註解標準:
package com.castronegro.contract;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
// InspectionRecord guarantees immutable audit records across rounds.
// NOTE: kept package-private here so this whole listing compiles as a single .java file;
// split into its own public file if you need it visible outside this package.
record InspectionRecord(int round, String targetPlayerId, boolean isWerewolf) {
public InspectionRecord {
Objects.requireNonNull(targetPlayerId, "targetPlayerId must not be null");
if (round < 1) {
throw new IllegalArgumentException("round must be positive: " + round);
}
}
}
// ContractViolationException represents an illegal operation breaching the protocol.
class ContractViolationException extends RuntimeException {
public ContractViolationException(String message) {
super(message);
}
}
// SeerInspectionContract defines invariants and boundaries required for any seer.
interface SeerInspectionContract {
InspectionRecord inspect(int currentRound, String targetPlayerId, boolean actualIdentity);
Map<String, InspectionRecord> getAuditLedger();
boolean hasInspected(String targetPlayerId);
}
// TrueSeer strictly respects immutability, state transitions, and round progression.
class TrueSeer implements SeerInspectionContract {
private final String seerId;
private int lastInspectedRound = 0;
private final Map<String, InspectionRecord> auditLedger = new HashMap<>();
public TrueSeer(String seerId) {
this.seerId = Objects.requireNonNull(seerId, "seerId must not be null");
}
@Override
public InspectionRecord inspect(int currentRound, String targetPlayerId, boolean actualIdentity) {
// Precondition 1: Monotonic round progression
if (currentRound <= this.lastInspectedRound) {
throw new ContractViolationException("Round regression detected. Last: " +
this.lastInspectedRound + ", Current: " + currentRound);
}
// Precondition 2: Self-inspection is forbidden
if (this.seerId.equals(targetPlayerId)) {
throw new ContractViolationException("Seer cannot inspect self: " + targetPlayerId);
}
// Precondition 3: Duplicate inspection verification
if (auditLedger.containsKey(targetPlayerId)) {
throw new ContractViolationException("Target already inspected: " + targetPlayerId);
}
InspectionRecord record = new InspectionRecord(currentRound, targetPlayerId, actualIdentity);
auditLedger.put(targetPlayerId, record);
this.lastInspectedRound = currentRound;
return record;
}
@Override
public Map<String, InspectionRecord> getAuditLedger() {
return Collections.unmodifiableMap(this.auditLedger);
}
@Override
public boolean hasInspected(String targetPlayerId) {
return auditLedger.containsKey(targetPlayerId);
}
}
// FakeWerewolfSeer attempts to fabricate conflicting records and hallucinates past rounds.
class FakeWerewolfSeer implements SeerInspectionContract {
private final Map<String, InspectionRecord> auditLedger = new HashMap<>();
@Override
public InspectionRecord inspect(int currentRound, String targetPlayerId, boolean actualIdentity) {
// Flawed logic: Accepts backward rounds and overwrites past claims arbitrarily
InspectionRecord fabricatedRecord = new InspectionRecord(currentRound, targetPlayerId, actualIdentity);
auditLedger.put(targetPlayerId, fabricatedRecord);
return fabricatedRecord;
}
@Override
public Map<String, InspectionRecord> getAuditLedger() {
return Collections.unmodifiableMap(this.auditLedger);
}
@Override
public boolean hasInspected(String targetPlayerId) {
return auditLedger.containsKey(targetPlayerId);
}
}
在這段程式碼中,TrueSeer 在內部封裝了完整的狀態檢查:
- 檢查查驗輪次是否單調遞增(防止狼人臨時補編昨晚的故事)。
- 杜絕查驗自己(神職能力限制)。
- 禁止對同一玩家重複查驗(防止前後發言矛盾)。
- 透過
Collections.unmodifiableMap返回不可變的稽核帳本,杜絕外部竄改。
相對地,FakeWerewolfSeer 忽視了前置條件防禦,允許任意竄改歷史紀錄。當整合測試在 CI/CD 流水線執行防禦性驗證時,缺乏約束的實作便會在多執行緒與契約驗證器面前原形畢露。
換句話說,TrueSeer 的稽核帳本是一份 append-only、對外唯讀的紀錄:寫進去的每一筆都追究得到,連一次「重來」都抹不掉它。而 FakeWerewolfSeer 的 auditLedger 可以被任意 put 覆蓋,歷史隨時能被改寫,於是任何謊言都說得通。介面契約的前置條件,就是把「可竄改」與「不可竄改」這條界線,畫死在編譯期。
介面契約與類別防禦架構圖
為了將契約約束視覺化,我們繪製出這張完整的 UML 類別關係圖。圖中清晰展示了抽象介面、不可變記錄物件以及具體實作在遇到非法狀態遷移時的防禦路徑:
classDiagram
direction TB
class SeerInspectionContract {
<<interface>>
+inspect(round: int, targetId: String, actualIdentity: boolean) InspectionRecord
+getAuditLedger() Map~String, InspectionRecord~
+hasInspected(targetId: String) boolean
}
class InspectionRecord {
<<immutable record>>
-round: int
-targetPlayerId: String
-isWerewolf: boolean
+round() int
+targetPlayerId() String
+isWerewolf() boolean
}
class TrueSeer {
-seerId: String
-lastInspectedRound: int
-auditLedger: Map~String, InspectionRecord~
+inspect(round: int, targetId: String, actualIdentity: boolean) InspectionRecord
+getAuditLedger() Map~String, InspectionRecord~
+hasInspected(targetId: String) boolean
-validatePreconditions(round: int, targetId: String) void
}
class FakeWerewolfSeer {
-auditLedger: Map~String, InspectionRecord~
+inspect(round: int, targetId: String, actualIdentity: boolean) InspectionRecord
+getAuditLedger() Map~String, InspectionRecord~
+hasInspected(targetId: String) boolean
}
class ContractViolationException {
+ContractViolationException(message: String)
}
SeerInspectionContract <|.. TrueSeer : implements
SeerInspectionContract <|.. FakeWerewolfSeer : implements
TrueSeer ..> InspectionRecord : creates
FakeWerewolfSeer ..> InspectionRecord : creates
TrueSeer ..> ContractViolationException : throws on invalid state
SeerInspectionContract ..> InspectionRecord : references
軟體工程意涵:拒絕 AI 幻覺的開源秩序
在現代軟體工程中,Apache Kafka 的核心客戶端設計(例如 ProducerRecord 與 RecordMetadata)即是介面契約與防禦性編程的經典典範。當生產者發送訊息時,底層核心會在第一時間校驗 Topic 分區合法性、Key/Value 序列化邊界與壓縮演算法相容性;一旦前置條件不符,立刻丟出例外,絕不在內部吞噬錯誤或靠直覺猜測。更關鍵的是,Kafka 的分區日誌本身就是一份 append-only、只能追加不能修改的紀錄——一則訊息一旦寫入某個 offset,就永遠釘在那裡,任誰都改不動。這正是七號那卷羊皮紙在工程世界裡的對應物。
在由「AI 狼」與「真人工程師」交織的開源博弈中,維護者不可能依靠通靈去辨別 PR 提交者的動機。唯一的解法,就是用嚴格的型別介面、語義契約與單元測試斷言,將行為邊界死死釘在編譯期與測試網格中。 當冒牌者企圖用一段邏輯自相矛盾的程式碼呼嚨過去時,介面契約會像一面照妖鏡,第一時間讓他在 CI 階段爆紅出局。
讀完今天這篇文章,你應該要能夠:在面對任何業務邏輯或 AI 生成的代碼片段時,主動為其抽象出高階介面契約,明確定義前置條件、後置條件與狀態不變量,並以防禦性編程原則全面封裝關鍵狀態。
參考資料與延伸閱讀
- OpenJDK - JDK 17(LTS)功能清單:
sealed類別、record、instanceof模式匹配——把「非法狀態不可表達」寫進語言層。 - OpenJDK - JDK 21(LTS)功能清單:
record解構模式、switch模式匹配、循序集合(Sequenced Collections)、虛擬執行緒。 - OpenJDK - JDK 25(LTS)功能清單:彈性建構子本體(Flexible Constructor Bodies,先驗參數再呼叫
super())、模組匯入宣告、精簡原始檔。 - Oracle - Java Language Changes(JDK 25,17→21→25 語言演進總覽)
- Hyperskill - Java Backend Developer (Spring Boot):4.5 分、228 小時、6 萬名學習者、含結業證書;用 Java + Spring Boot 從物件導向基礎一路做到可上線的 Web 應用。
- Eiffel - Design by Contract, Assertions and Exceptions:Bertrand Meyer 契約式設計的一手來源,前置/後置條件與不變量的原始定義。
- 關鍵檢索詞(建議使用 Felo.ai / Perplexity 查詢):
"Design by Contract Bertrand Meyer invariants preconditions" "Java 21 record patterns sealed classes" "JDK 25 flexible constructor bodies validate before super" "defensive programming fail fast immutable record"
¹ 註:本書名為情境設定之虛構文獻,非真實歷史或開源紀錄。