AI開発で意図しない実装を減らすには? DDDとフールプルーフでドメインを堅牢にする
AIに機能の実装を頼んだのに、求めていた振る舞いと違うものができる。AIを使った開発では、そんな経験があるのではないでしょうか。たとえば、注文の確定時に明細を確認する confirm() が用意されているのに、AIが data class の copy() で状態だけを変えるコードを追加する、といったケースです。
これはAIが意図的にルールを破ろうとしたというより、モデルが許している操作の中から、要件に合わない実装を選んだ結果です。そこで、DDDでドメインルールを表現し、フールプルーフの考え方でルールを破る操作自体を選びにくくしてから、AIにサービスの機能を実装させる進め方を考えます。
ここでいう「AI駆動開発」は、AIにコード生成や変更を依頼し、人が要件と成果物を確認しながら開発することを指します。
AIに任せる前に、守るべきルールを決める
Section titled “AIに任せる前に、守るべきルールを決める”注文を例に、まず次のルールを言葉にします。
- 数量は1以上99以下
- 同じ商品を一つの注文に二重登録できない
- 明細がない注文は確定できない
- 確定済みの注文は変更できない
この段階で、作りたい画面やAPIだけでなく、作れてはいけない状態も明確にします。たとえば「注文登録に成功すること」だけをAIに伝えると、どの条件で登録を拒否するかが実装者の推測に委ねられます。
ルールが曖昧なら、先に業務の担当者と確認します。ドメインモデルは、まだ決まっていない業務ルールを自動で決めてくれるものではありません。
ルールを守る操作だけを使えるようにする
Section titled “ルールを守る操作だけを使えるようにする”値単体の条件は Quantity のような値オブジェクトが生成時に確認します。明細の重複や状態遷移は Order 集約ルートが確認します。注文の状態や明細の可変コレクションを外へ公開せず、addItem() や confirm() のような操作だけを入口にします。
たとえば、通常の data class Order(val status: OrderStatus, ...) では、生成された copy() で状態を変えた新しい注文を作れます。AIが confirm() の代わりに order.copy(status = OrderStatus.CONFIRMED) を選べば、確定条件を確認しない実装になり得ます。集約に任せたいルールがあるなら、単なるデータの入れ物として公開しないことが大切です。
enum class OrderStatus { DRAFT, CONFIRMED }
class Order private constructor( val status: OrderStatus, private val items: List<String>,) { fun confirm(): Order { check(status == OrderStatus.DRAFT) { "すでに確定済みです" } check(items.isNotEmpty()) { "明細がない注文は確定できません" } return Order(OrderStatus.CONFIRMED, items) }
companion object { fun draft(): Order = Order(OrderStatus.DRAFT, emptyList()) }}この例では通常の class と private constructor を使い、外部から任意の状態の Order を作る操作や、自動生成される copy() を公開していません。状態を変える入口は、条件を確認する confirm() に限られます。実際のモデルでは、復元や明細追加など必要な操作も、同じように不変条件を保つ入口として設計します。
flowchart LR
A["AIが追加するAPI・バッチ"] --> B["アプリケーションサービス"]
B --> C["OrderRepository"]
C --> D["Order 集約ルート"]
D --> E["不変条件を確認して状態を変更"]
たとえば注文確定APIを追加するとき、望ましい経路は次の形です。
val order = orderRepository.findById(orderId) ?: error("注文が見つかりません")val confirmedOrder = order.confirm() // 条件を確認して確定済みの注文を返すorderRepository.save(confirmedOrder)これに対し、次のような実装は集約のルールを通りません。
// 避けたい例:ドメインを復元せず、状態を直接更新するorderTable.updateStatus(orderId, "CONFIRMED")前者では、APIを追加するたびに「明細はあるか」という条件を呼び出し側へ書き直す必要がありません。confirm() の実装を共通の入口にできれば、人が書いた処理もAIが書いた処理も同じルールを通ります。これがドメインモデルにフールプルーフを応用する利点です。
AIに渡す設計の材料
Section titled “AIに渡す設計の材料”ドメインの骨格を用意したら、機能ごとに次の材料をAIへ渡します。
| 材料 | 注文確定APIで伝える内容 |
|---|---|
| ユースケース | 指定した下書き注文を確定する |
| 正規の更新経路 | OrderRepository で取得し、Order.confirm() の戻り値を保存する |
| 避けたい実装 | copy() やテーブル直接更新で、集約の操作を通らない変更を加えない |
| 失敗条件 | 注文がない、明細がない、すでに確定済み |
| 確認方法 | 成功時の状態と、各失敗条件で保存されないことを確かめる |
たとえば、依頼文は次のように具体化できます。
注文確定APIを追加してください。
OrderRepositoryから注文を取得し、Order.confirm()の戻り値を保存してください。copy()や注文テーブルの直接更新で状態を変える処理は追加しないでください。明細なし・確定済み注文では保存されないことをテストしてください。
ただし、依頼文は設計を補助するものです。プロンプトに「直接更新しない」と書くだけでは、別の依頼や将来の変更で別の更新経路が追加される余地が残ります。公開API、モジュールの依存関係、DB権限などにも意図を反映します。
ドメインからサービスへ、少しずつ広げる
Section titled “ドメインからサービスへ、少しずつ広げる”集約や値オブジェクトは、業務ルールを最終的に守る中核です。どの状態を許し、どの操作で状態を変えるかは、人が業務要件と照らして決め、実装とテストを確認します。AIに案やテストを作らせることはできますが、ドメインの中核を自由に書き換えさせるのではなく、人が確認した変更だけを取り込みます。
ドメインモデルが固まったら、AIにはその公開された操作を使うアプリケーションサービス、API、バッチなどを実装させます。作業対象を指定できる環境では、集約や値オブジェクトを編集対象から外すか、変更する場合に理由と差分の人手確認を必須にすると、役割分担が明確になります。新しい要件のためにドメイン変更が必要になったときは、いったんその変更を人が判断し、モデルとテストを更新してから機能実装へ戻ります。
全ドメインを最初に完成させる必要はありません。まず一つのユースケースについて、次の順に進めます。
- 業務ルールと不正な状態を整理する。
- 人が値オブジェクトと集約ルートに不変条件を実装する。
- 不正な生成や状態遷移が拒否されることを確認する。
- 正規の更新経路を使って、AIにAPI・バッチなどの機能を作らせる。
- 差分を確認し、ドメイン中核への意図しない変更やルールの重複がないか調べる。
次のユースケースで新しいルールが分かったら、モデルとテストを更新してから機能を広げます。こうすると、AIが作る各機能に共通の土台を育てながら開発できます。
設計だけでAIの誤実装をなくせるわけではない
Section titled “設計だけでAIの誤実装をなくせるわけではない”ドメインの入口を絞っても、AIがリポジトリ全体を編集でき、直接SQLや新しい保存処理を自由に追加できるなら、設計だけで誤実装を完全になくすことはできません。既存の集約メソッド自体を変更してルールを弱めることも可能です。
そこで、ドメインの不変条件に加え、次の境界を組み合わせます。
- コードの境界 — APIやバッチから永続化実装へ直接依存させず、更新は集約とリポジトリを通す。
- 保存時の境界 — 表現できるルールには一意制約などを使い、同時更新にはトランザクションや競合検出を検討する。
- 作業の境界 — AIの変更差分で、ドメインルールや保存経路が変わっていないかを人が確認する。
- 実行時の境界 — AIにDB操作も任せる場合は、利用可能な権限やツールを作業に必要な範囲へ絞る。
フールプルーフは、AIの判断が常に正しいことを前提にしません。意図と違う実装を選びにくくし、残る間違いを早く見つけられる構造を目指します。
AI駆動開発では、人がドメインの不変条件と、それを守る操作を集約として固めてから、AIにその操作を使うサービスの機能を実装させる方法が有効です。AIに毎回ルールを再現させるより、ルールを持つ操作を使わせる方が、求める振る舞いに沿った実装を得やすくなります。
一方で、AIがコードやDBへ自由にアクセスできるなら、ドメイン設計だけでは完全な強制力になりません。モデル、依存関係、保存時の制約、テスト、変更レビューを合わせて設計します。
- Martin Fowler: DDD Aggregate — 集約ルートを整合性の入口にする考え方
- ASQ: What is Mistake Proofing? — 誤りの予防と即時検出の考え方
- Kotlin: Data classes —
data classが生成するcopy()などの関数 - GitHub Docs: Review AI-generated code — AI生成コードを確認する際の観点