iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
佛心分享-IT 人自學之術

狼人自爆的心路歷程:一個「AI人」的30天自學修煉系列 第 4

Day 04|雙預言家生推對決:介面契約、物件導向四柱與防禦性編程

  • 分享至 

  • xImage
  •  

「有人立過誓,也有人毀過誓。夜色帶得走一句口頭的承諾,帶不走一行寫進帳本的字。」
——《阿帕契開源審計錄》¹ 卷一·契約篇

幕間
女巫終於決定用毒,倒向一個白天發言老是慢半拍的座位。
天亮,法官只報那個座位夜裡沒了人——沒有翻牌,沒有遺言,誰也不知道倒下的是狼還是好人。
「我到底毒對了沒有?」女巫在帷幕後發抖,這個問題她永遠得不到答案。獵人按住她:「別聲張,還有預言家。他昨晚驗的結果,寫下來了。」

城堡大廳的壁爐燃燒著劈啪作響的松木,清晨的鐘聲剛落,法官那冷峻的聲音穿透了厚重的晨霧:「昨夜平安夜,現在進入警長競選階段。」

這是標準九人局(三隻狼人、三位神職、三個平民)中最驚心動魄的局面——經典殘局之六:「雙預言家生推對決」。在屠邊規則的高壓下,手握 1.5 票生殺大權的警徽成了兩大陣營必爭的咽喉要道。全場貫徹嚴格的「絕發規則」,每個人只能在自己的輪次發言,無人得以插話。此時,長桌對面的 2 號玩家猛然舉起手牌起跳:「我是預言家,昨晚驗了 4 號是金水(好人),警徽流先 7 後 9!」

還沒等大家消化這個資訊,坐在我身旁的 5 號玩家冷笑了一聲,毫不猶豫地拍下桌子:「真預言家在此!昨晚查驗 2 號是個大鐵狼!他是在悍跳搶警徽,企圖給隊友發金水做身份!」

雙預言家對跳!

如果是在前幾世,身為 AI 魁儡的我肯定又會手足無措地向模型求救,生成幾段聽起來慷慨激昂、實則毫無邏輯支撐的辯詞。然而,在經歷了無數次被女巫無聲毒殺與白天公投放逐的死循環後,我們 2N1P 團隊清醒地意識到:悍跳狼之所以能用 AI 幻覺在發言中混淆視聽,根源在於系統缺乏不可妥協的「介面契約」與「狀態封裝」。 當任何人都能在發言時隨心所欲地竄改查驗歷史、捏造不存在的狀態遷移時,謊言便能在沒有編譯期防護的荒野中肆意生長。


口頭的判決會蒸發,寫下的紀錄不會

那天散場後,我在廊下想了很久。

女巫那個永遠得不到答案的問題——「我到底毒對了沒有」——之所以無解,是因為那一夜沒有人翻牌、沒有人把結果寫下來。凡是只活在記憶和口頭裡的東西,隔一個晚上就可能對不上,甚至會被人悄悄改掉一筆而你渾然不覺;唯有被鄭重寫進一份誰都動不了的紀錄,那句話才追究得到。

七號那卷從不離身的羊皮紙,好像就是這樣一份東西——她記下的字,我從沒見過哪一輪被改動。而今天這場雙預言家對跳賭的也是同一件事:誰的查驗紀錄,是寫進了不可竄改的契約裡的?


物件導向四本柱:為混亂的世界築起邊界

在軟體工程領域,特別是面對大型開源專案與複雜的分散式架構時,物件導向程式設計(Object-Oriented Programming, OOP)的四大支柱不是教科書上的死背名詞,而是保護系統不被惡意操作或非預期狀態摧毀的護城河:

  1. 封裝(Encapsulation):將物件的內部狀態(State)隱藏在邊界之內,僅透過受控的公開方法(Public Methods)暴露能力。預言家昨晚查驗的名冊與時間戳記,絕不能是外部可以隨意寫入的公開欄位;一旦狀態失去封裝,冒牌者便能在白天隨時修改查驗目標。
  2. 抽象(Abstraction):將複雜的現實世界運作抽離為清晰的核心特徵。我們不在乎查驗過程是透過水晶球還是直覺,我們只定義查驗行為必須產生的保證——給予一個目標,回傳一個確定且不可竄改的判定結果。
  3. 繼承(Inheritance):建立行為規範的階層關聯,實現代碼與特性的複用,但必須嚴格遵守里氏替換原則(Liskov Substitution Principle, LSP)。子類別不能破壞父類別所承諾的任何先決條件。
  4. 多型(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.
public 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.
public 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、對外唯讀的紀錄:寫進去的每一筆都追究得到,連一次「重來」都抹不掉它。而 FakeWerewolfSeerauditLedger 可以被任意 put 覆蓋,歷史隨時能被改寫,於是任何謊言都說得通。介面契約的前置條件,就是把「可竄改」與「不可竄改」這條界線,畫死在編譯期。


介面契約與類別防禦架構圖

為了將契約約束視覺化,我們繪製出這張完整的 UML 類別關係圖。圖中清晰展示了抽象介面、不可變記錄物件以及具體實作在遇到非法狀態遷移時的防禦路徑:

https://ithelp.ithome.com.tw/upload/images/20260910/201836841y6c6Tvx5h.png


軟體工程意涵:拒絕 AI 幻覺的開源秩序

在現代軟體工程中,Apache Kafka 的核心客戶端設計(例如 ProducerRecordRecordMetadata)即是介面契約與防禦性編程的經典典範。當生產者發送訊息時,底層核心會在第一時間校驗 Topic 分區合法性、Key/Value 序列化邊界與壓縮演算法相容性;一旦前置條件不符,立刻丟出例外,絕不在內部吞噬錯誤或靠直覺猜測。更關鍵的是,Kafka 的分區日誌本身就是一份 append-only、只能追加不能修改的紀錄——一則訊息一旦寫入某個 offset,就永遠釘在那裡,任誰都改不動。這正是七號那卷羊皮紙在工程世界裡的對應物。

在由「AI 狼」與「真人工程師」交織的開源博弈中,維護者不可能依靠通靈去辨別 PR 提交者的動機。唯一的解法,就是用嚴格的型別介面、語義契約與單元測試斷言,將行為邊界死死釘在編譯期與測試網格中。 當冒牌者企圖用一段邏輯自相矛盾的程式碼呼嚨過去時,介面契約會像一面照妖鏡,第一時間讓他在 CI 階段爆紅出局。

讀完今天這篇文章,你應該要能夠:在面對任何業務邏輯或 AI 生成的代碼片段時,主動為其抽象出高階介面契約,明確定義前置條件、後置條件與狀態不變量,並以防禦性編程原則全面封裝關鍵狀態。


參考資料與延伸閱讀


¹ 註:本書名為情境設定之虛構文獻,非真實歷史或開源紀錄。


上一篇
Day 03|規則即邊界:有限狀態機、枚舉型別與 Fail Fast 哲學
下一篇
Day 05|獵人的核威懾:Big-O 複雜度分析與演算法效能的致命代價
系列文
狼人自爆的心路歷程:一個「AI人」的30天自學修煉5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言