はじめに
Design by Contract(以下、DbC:契約による設計)は、ソフトウェアの振る舞いを「契約」に基づいて設計する強力なアプローチです。
契約は主に以下の3つで構成され、モジュールの「入り口」と「出口」の仕様を明示してプロセスの保証範囲をクリアにします。
- 事前条件(Precondition)
- 事後条件(Postcondition)
- 不変条件(Invariant)
仕様やビジネスルールをコード上の厳密な規約として表現できる点において、DbCのメリットは極めて明白です。
しかし疑問が残ります。ドメイン駆動設計(DDD)やClean Architectureが浸透し、「契約」「不変条件」「境界」「責務」といった概念がこれほど重視されているにもかかわらず、「アプリケーション全体をDbCで記述する設計」はなぜ一般的にならないのでしょうか?
私は、その原因はDbC自体の価値ではなく、「契約を記述・配置するための構造的な表現方法」にあったと考えています。
本稿で解説するトポロジー契約チェーン(Topological Contract Chain:以下、TCC)は、「アプリケーション全体を有向非巡回グラフ(DAG)として記述し、その構造の上にDbCを同時に組み込む 」という設計アプローチです。なぜこの統合が必要なのか、順を追って紐解きます。
1. DbCが持つ4つの本質的メリット
改めて整理すると、DbCには単なるバリデーションを超えた4つの強みがあります。
1-1. ドメイン知識の厳密な明文化
DbCの最大の価値は、ドメイン知識をあやふやな自然言語ではなく「ソフトウェアが守るべき厳密なルール」として依存グラフ(DAG)に固定できる点です。
例えば、注文確定後に行う出荷指示という処理を考えてみます。
- Precondition: 注文が確定しており、かつ在庫が確保されていること
- Postcondition: 出荷指示データが作成されていること
この契約を書くことで、「この処理はどういう状態のときに実行可能なのか」、そして「実行した結果として何が保証されるのか」というドメインの核心がそのまま明示されます。
1-2. UI/UXとのダイレクトな連動
ドメインルールは、そのまま画面の制御ルール(UI/UX)に直結します。
- 期限切れの試薬は持ち出せない
- 廃棄済みの試薬は持ち出せない
- 返却されていない試薬は持ち出せない
これらは不変条件や業務上の絶対的な事実(ビジネスルール)であり、同時に「画面上でボタンを活性化させるか」というUI制御の根拠そのものです。
1-3. AI Codingにおける「コンポーネント境界」の提供
AIエージェントにコードを書かせる際、DbCは最強の指示書になります。
サブエージェントにタスクを渡す際、事前条件・事後条件・不変条件をセットで渡すことで、AIの探索範囲が明確に限定されます。ハルシネーションを起こしようがないレベルで精密な実装を促すことが可能です。
1-4. テストケースの自動導出
契約が定義されていれば、テストケースは機械的に導出できます。
$$\text{Preconditionを満たす} \longrightarrow \text{処理実行} \longrightarrow \text{Postconditionを満たす(正常系)}$$
$$\text{Preconditionを満たさない} \longrightarrow \text{処理不能・拒絶(異常系)}$$
つまりDbCを導入すると、以下の思考ラインが一筋の線でつながります。
$$\text{ドメイン知識} \longrightarrow \text{契約} \longrightarrow \text{実装} \longrightarrow \text{UI} \longrightarrow \text{テスト}$$
2. なぜDbCはアプリ全体に普及しなかったのか
これほど強力な概念でありながら、なぜアプリケーション全体の標準設計になり得なかったのでしょうか。
2-1. 局所的契約から「全体整合性」への壁
問題は「メソッド単位の局所的なDbC」を書くのが精一杯で、「アプリ全体の契約の整合性」を保つ難易度が跳ね上がる点にありました。
単一のメソッドであれば、以下のように記述できます。
A() ├─ Pre: ...
└─ Post: ...
B() ├─ Pre: ...
└─ Post: ...
しかし、実際のアプリケーションは A → B → C → D → E という複雑な依存の連鎖で動いています。ここで次のような構造的問いが発生します。
- 「AのPostconditionは、本当にBのPreconditionを充足しているか?」
- 「BとCの間に存在するドメイン上の契約は整合しているか?」
- 「Cが保証した状態を、Dはどの条件として利用しているのか?」
個々の関数に契約を書くこと以上に、「契約同士をアプリ全体で矛盾なく繋ぎ合わせること」が極めて困難だったのです。
2-2. 認知負荷と「ベテランの頭の中」依存
実務において、優秀な設計者やベテランエンジニアはこれらを頭の中で処理しています。
「この処理の前にはこの状態が必要で、終わればこの状態が保証される。だから次の処理へ渡せる。ただし、このルートから到達した場合は別だ……」
彼らは「依存関係」と「契約」を無意識に同時に脳内モデリングしています。ドメイン知識が十分にあれば、DbC、実装、テスト、エラーハンドリング、状態遷移までを一気貫通で設計できてしまいます。
しかし、これは破格の認知負荷を個人に強いるアプローチです。頭の中にシステム全体の依存グラフを保持しながら、すべてのエッジに契約を配置し続けるのは人間にできる限界を超えています。結果としてDbCは「複雑な箇所の局所的なガード」にとどまり、属人化を抜け出せなかったと考えられます。
3. 解決策:DAGとDbCの一体化(TCCの思想)
この限界を突破するのが、トポロジー契約チェーン:Topological Contract Chain(以下TCC)のアプローチです。
3-1. 依存関係(DAG)のエッジに契約を載せる
まず、アプリケーション全体の依存関係を有向非巡回グラフ(DAG)として捉えます。
$$A \longrightarrow B \longrightarrow C$$
ただし、DAG単体はあくまで「依存の方向」を示しているに過ぎず、それ自体が契約を意味するわけではありません。そこで、DAGのエッジ(矢印)に対して直接「契約(Contract)」を配置します。
$$A \xrightarrow{\text{Contract AB}} B \xrightarrow{\text{Contract BC}} C$$
- ノード: 状態、データ、処理、フォームなど
- エッジ: 依存関係
- Contract: その依存関係(遷移)が成立するための条件と保証
これがTCCの核心です。ユーザーが語るドメイン知識には契約となるドメインルールやビジネスルールが含まれるので、DAGとDbCを別々に設計するのをやめ、「DAGという依存の骨格の上に、DbCを配置する」ことで両者を一体化させます。
3-2. 「契約なき依存エッジを許さない」原則
構造上に $A \longrightarrow B$ という依存が存在するなら、そのエッジには必ず以下が定義されます。
A ──→ B
Pre: Bを実行するために必要な条件
Post: AがBに対して保証する条件
契約は単なる付け足しの注釈ではなく、「そのエッジを通過できるかどうかを決める条件そのもの」です。
$$A \xrightarrow{\text{Contract}} B$$
すなわち、「依存関係は存在するが、Contractを満たさない限りその依存関係は利用できない」という意味になります。TCCでは「契約が未定義のエッジ(実行経路)は未完成であり、存在を許さない」という厳格な設計原則を置きます。
4. 宣言型ガードと具体例
4-1. 手続き型ガードから「リアルタイム評価」へ
従来のアプリケーションでは、ガード処理(事前チェック)は処理の直前に手続き型で書かれます。
[持ち出しボタンを押す]
↓
ガード節(Ifチェック)
- 廃棄されていない?
- 持ち出されていない?
- 期限切れではない?
↓
持ち出し処理を実行
これは「実行直前に手前で割り込んでチェックする」という手続き型の発想です。
対して、TCCによる宣言型モデルでは以下のように表現されます。
[試薬瓶]
│
├────→ [履歴]
│
├────→ [残量]
│
└────→ [持ち出し可能]
│
│ Contract
↓
[持ち出し]
ここで「持ち出し可能」という状態は、試薬瓶の属性から宣言的に導出されます。
$$\text{持ち出し可能} = \text{未廃棄} \land \text{未持ち出し} \land \text{有効期限内}$$
この条件はリアルタイムに評価され続けます。例えば時刻が経過して期限切れになった瞬間、有効期限内 = false となり、自動的に 持ち出し可能 = false へと即座に伝播します。
結果として「持ち出しエッジ」そのものが不成立(切断)になります。「処理の直前にエラーを弾く」のではなく、「契約が常に状態をリアルタイム評価し、経路の可否を制御する」 のです。
4-2. 試薬管理モデルで解説する状態遷移と契約
契約の中身を整理すると、大きく2つの要素に分類できます。
- 不変条件(Invariant): エンティティとして常に満たすべき状態の事実(例:未廃棄、未持ち出し)
- ビジネスルール(Business Rule): 業務上の運用ポリシー(例:有効期限内であること、劇物の場合は重量計測を必須とすること)
従来の設計では、不変条件はエンティティ内部に、ビジネスルールはアプリケーションサービスやUIのバリデーションロジックに分散しがちでした。TCCではこれらを「エッジ上の契約」として統合します。
$$\text{Contract} = \text{Invariant} \land \text{Business Rule}$$
「Submit後にはねる」のではなく「Form起動すらさせない」
ユーザーがフォームで入力し、「確定(Submit)」ボタンを押した直後にサーバー側でエラーを返す……これが従来のやり方でした。
しかし、DAGのエッジに契約を配置すると、制御はUIの起点で発動します。
$$[\text{試薬瓶}] \xrightarrow{\quad\text{Contract}\quad} [\text{持ち出しForm}]$$
このエッジは常にリアルタイム評価されています。試薬が期限切れになったり、他者によって持ち出された瞬間に Contract は false となり、「試薬瓶 → 持ち出しForm」の依存エッジ自体が遮断されます。
これにより、UI層では「持ち出しFormを開くイベント」自体が発生し得なくなります。処理の途中でエラーを吐くのではなく、「不可能な遷移経路をそもそも存在させない(到達不能にする)」という圧倒的な安全性が手に入ります。
例:ビジネスルールによる「経路の解放」と「フォーム制約の自動決定」
ビジネスルールは、操作を止めるためだけではなく、「特定の状態になった際に、正しく次の経路(ノード)を解禁する」役割も果たします。
- 持ち出し経路: 期限切れの試薬は「持ち出し」できません(契約 $false$)
- 廃棄経路: しかし、期限切れや使い切った試薬は「廃棄」されるべきであり、廃棄ノードへの経路が正しく解禁(契約 $true$)されます
【通常状態の試薬瓶】
[試薬瓶] ──[ Contract: 未期限切れ ]───→ [持ち出しForm]
[試薬瓶] ──[ Contract: 期限切れ ]──x (遮断) ──→ [廃棄Form]
【期限切れの試薬瓶】
[試薬瓶] ──[ Contract: 未期限切れ ]─x (遮断) ──→ [持ち出しForm]
[試薬瓶] ──[ Contract: 期限切れ ]───→ [廃棄Form] (解禁)
状態に応じて、DAG上でどのノードに到達可能か(Reachability)が宣言的に切り替わります。
エッジの契約がFormのプロパティ(Required)へ伝播する
さらに強力なのは、エッジを流れるコンテキスト(ビジネスルール)が、遷移先であるFormの入力制約を自動的に決定する点です。
例えば「劇物」を扱う場合、「使用前後に必ず重量を秤量しなければならない」というルールがあるとします。これをUI側の if (isPoisonous) などの条件分岐で書くのではなく、エッジの契約から伝播させます。
【通常試薬】
[試薬瓶] ──[ Contract: 通常 ]──→ [持ち出しForm]
└─ CurrentWeight.Required = false
【劇物試薬】
[試薬瓶] ──[ Contract: 劇物 ∧ 重量測定必須 ]──→ [持ち出しForm]
└─ CurrentWeight.Required = true
「劇物である」という文脈を持つエッジを通ってFormが起動する際、Formの CurrentWeight フィールドは自動的に Required = true としてバインドされます。
✕ アンチパターン(Form内部で直接判定)
Required = If(SelectedItem.IsPoisonous, true, false)
これを行うと、UIの中にビジネスルール(劇物判定)が漏れ出し、手続き型の複雑さが画面コード内に再生産されます。
〇 理想的な設計(契約のバインド)
Required = EdgeContract.IsWeightRequired
フォーム側は「なぜ必須なのか」という理由を知る必要はなく、単に 「受け取った契約で必須と定義されているから Required = true にする」 というUIの挙動に専念できます。
このように不変条件とビジネスルールをDAGのエッジとして表現することで、①どのノードへ遷移できるか(到達可能性)と②遷移先でどんな制約が生じるか(入力条件)の双方が、手続き型ロジックに頼ることなく構造から一貫して保証されます。
5. TCCがもたらす設計の拡張性
5-1. アプリケーション全レイヤーへの波及
この考え方は、アーキテクチャの全レイヤーにそのまま適用できます。
$$\text{Domain} \longrightarrow \text{Application} \longrightarrow \text{Infrastructure} \longrightarrow \text{UI}$$
各レイヤー間の依存関係に契約を持たせることで、DbCは「単に入力値を検証する局所的な技法」から、「アプリケーション全体の状態遷移と依存関係を成り立たせるためのフレームワーク」へと進化します。これで初めて、「アプリ全域をDbCで記述する」という試みが現実的な意味を持ちます。
5-2. AI Coding(サブエージェント)との抜群の相性
TCCの構造は、LLMやAIエージェントによる自動生成と極めて高い親和性を誇ります。
従来のように「試薬持ち出し機能を実装して」と丸投げするのではなく、DAGとContractが定義されていれば、タスクは以下のように厳密に分解されます。
Node:
ReagentBottle
Edge:
ReagentBottle → TakenOut
Pre:
¬Disposed ∧ ¬TakenOut ∧ ExpiryDate >= Today
Post:
TakenOut = true
思考の流れは次のようになります。
$$\text{DAG} \longrightarrow \text{Contract} \longrightarrow \text{Sub-agent Task} \longrightarrow \text{Implementation}$$
AIに「何をすべきか」から考えさせるのではなく、「どの契約を満たすノードを実装するか」という完全な境界を与えて作業を分担できます。
また、同じ契約からテストケースも一意に自動導出できます。
Precondition: 未廃棄 AND 未持ち出し AND 有効期限内
【正常系テスト】
✓ 未廃棄 + 未持ち出し + 有効期限内 ──→ 持ち出し成功
【異常系テスト】
✗ 廃棄済み ──→ 拒絶
✗ 持ち出し済み ──→ 拒絶
✗ 期限切れ ──→ 拒絶
【境界値テスト】
・有効期限 = 今日 / 昨日
・残量 = 0
$$\begin{array}{rcc} & \text{Domain Knowledge} & \\ & \downarrow & \\ & \text{DAG} & \\ & \downarrow & \\ & \text{Contract} & \\ \swarrow & & \searrow \\ \text{実装} & & \text{テスト} \end{array}$$
開発プロセス全体が単一の構造から自然に導出される形になります。
5-3. コンパイル標的としての「言語非依存性」
ここで最も重要なのは、「DAG + Contract」のモデルは特定のプログラミング言語に依存しないという点です。
ReagentBottle
│
│ TakeOut
├─ Pre: NOT Disposed AND NOT TakenOut AND ExpiryDate >= Today
└─ Post: TakenOut = true
この抽象モデルは、Power Apps、Python、TypeScript、Rustのいずれで実装しようと一切変わりません。
$$\begin{array}{ccccc} & & \text{DAG + Contract} & & \\ & \swarrow & \downarrow & \searrow & \\ \text{Power Apps} & & \text{Python} & & \text{TypeScript} \end{array}$$
「DAG + Contract」は実装コードよりも一段高いレイヤーに位置する上位表現(モデル)です。実装時には、ターゲットとするターゲット言語やフレームワークに最適な形へ「コンパイル(変換)」すればよい。これは、手続き型コードを直接AIに書かせる手法とは根本的に異なるアプローチです。
6. まとめ:既存の設計概念を「エッジ上のContract」へ統合する
DbC(契約による設計)がこれまで普及しなかったのは、DbCという概念そのものに価値がなかったからではありません。
- ドメイン知識の厳密な明文化
- 実行条件の宣言的表現
- 到達可能性(Reachability)による動的な状態・機能の制御
- AIエージェントへの明確なコンポーネント境界の提供
- テストケースの自動導出
これだけの絶大なメリットがありながら普及を阻んでいた最大の要因は、**「オブジェクト内部(メソッドやクラス)に閉じた局所的な契約しか記述できず、アプリ全体の依存構造と一体化するフレームワークが存在しなかったこと」**にあります。
TCC(Topological Contract Chain)は、何か突飛な新しいプログラミング言語やアルゴリズムを発明するものではありません。
「世界の事実(Node)」と「その利用条件(Edge)」を明確に分離し、これまで現場でバラバラに扱われていた既存の設計概念を、「DAGのエッジに契約(Contract)を載せる」というたった1つの構造に統合(射影)する記述アプローチです。
- データフロー: ノード間の接続(A $\longrightarrow$ B)
- 状態遷移: ノード間の状態変化
- DbC: エッジに付与された成立条件(Precondition / Postcondition)
- リアクティブ: 状態変化に伴うエッジ評価(到達可能性)の自動再計算
$$\text{世界の事実 (Node)} \xrightarrow{\quad \text{Edge Contract (Reachability)} \quad} \text{次の利用・表現 (Node)}$$
データを壊れていない「事実」としてノードに保持させ、それが「どの文脈・処理で利用できるか」をエッジ上の契約(Contract)として宣言する。
「DbCをコードの横に付け足すバリデーションではなく、依存関係(エッジ)そのものの成立条件として一体化させる」
これが、TCCが提案するDbCの実戦的な再定義です。
おわりに
DbCは、単なるエラーチェックやバリデーション技法ではありません。「ある状態から次の状態へ進むために、何が保証されるべきか」というソフトウェアの本質的な関係性を記述する手法です。
アプリケーション全体を依存グラフ(DAG)として捉え直すとき、契約はコードの内部ではなく、ノード間のエッジへと自然に収まります。
$$\text{ドメイン知識 (Fact)} \longrightarrow \text{依存関係 (DAG)} \longrightarrow \text{エッジ上の Contract} \longrightarrow \text{UI / 実装 / テスト / AI指示}$$
「廃棄されたデータ」をDBから消し去る必要も、不変条件違反としてシステムから排除する必要もありません。データは正当な事実としてノードに存在し続け、エッジの契約評価(Reachability)が $false$ になることで、安全に隠ぺい・保護されるだけです。
1枚のエッジ契約(Edge Contract)が定義されれば、UIの有効/無効制御、バックエンドのガード節、異常系テストケース、そしてAI Codingへのコンポーネント境界が、すべて同じグラフ構造から決定論的に導出されます。
人間が頭の中で巨大な依存関係と条件分岐を保持し続ける必要はありません。
DbCを「あとからコードに追加する注釈」から、「アプリケーションの依存関係を成立させるエッジそのもの」へ。
これまで別々に扱われていた設計概念を一枚のグラフの上に美しく収めること――これこそが、TCCが提示する設計の姿です。