0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI駆動開発で継続的にメンテナンス可能なシステムを作る「契約駆動開発」

0
Posted at

背景

codexclaude code などのAIエージェントを利用することにより、ソースコードを作成することは、かなり容易になりました。しかし一方で、システムとして一定規模で実装した後の継続的なメンテナンスについては常に不安が付きまとっていると思います。

私も、AIでソースコードはどんどん作ることができるものの、一体どうすれば、AIによる変更を繰り返してもエントロピーの増大を制御できる、継続的にメンテナンス可能な「固い」システムを作れるのかを試行錯誤していました。

初めはMDの仕様書を作り、クラス図、シーケンス図を作って…と、既存開発フローをあてはめたりと色々試したのですが、結局AIが仕様をうまく読み取れていないような挙動であったり、良かれと思ったシーケンス図やクラス図が実はAIの思考を逆に固定してしまうことで、スムーズにリファクタリングが出来なくなってしまうのでは?と思い始めました。

そこで、究極は 外部仕様が固まっていれば、中身をどれだけ書き換えても問題ない 方式がAIにとってはベストではないか、さらにAIが読むのであれば、構造化すべきドキュメントはMDよりも構造が固定されたDSLで定義してしまった方が良いのでは?と方向転換し、AI駆動開発で実践可能な契約駆動開発のベースが出来上がりました。

そこから、理想的には外部仕様を固定するだけで十分だが、実務では内部実装を部分的にメンテナンスする必要がどうしても生じる、との結論に達しました。

そのため、外部仕様だけでなく、内部設計にも最低限の方向付けが必要だと考え、DDD(ドメイン駆動設計)の分析結果をもとに契約とモジュール構成を導き、そのルール内での実装を促す方式としました。
これにより、内部実装が無法地帯になることなく継続的なメンテナンスが可能になったと思います。

ここでは、その手法についての解説をしていきます。

1. 業務分析

DDDによって業務を分析し、業務上の責務、用語、ルールが一貫する業務境界(Bounded Context)を識別します。

Bounded Context内で契約を定義する領域を、契約対象として定めます。契約対象には、例として、注文受付、注文照会、在庫確保などが考えられます。

一つのBounded Contextに、契約対象を一つだけ定めることも、複数定めることもあります。

画面、API、バッチ処理などは契約対象そのものではなく、契約対象が外部に振る舞いを提供する接点として整理します。一つの契約対象が、画面とAPIのように複数の外部接点を持つこともあります。

業務と契約対象の例

例えば、注文管理には複数の契約対象を定め、在庫管理には一つの契約対象を定めることができます。

契約対象は、AIとの対話を通じて分析し、導き出していきましょう。

2. 外部設計

契約対象ごとに契約を定義します。

ここで言う契約とはざっくり言うと、

「契約対象が外部に要求する条件と、その条件下で外部に対して保証する振る舞いについての決めごと」

のことです。

「〇〇という条件が満たされたら、〇〇という振る舞いをする」

という、外部から観測・検証可能なルールの集合と捉えてください。

契約は仕様の一部であり、仕様のうち、成立条件と、その条件下で保証される外部から観測・検証可能な結果を定義した部分です。

契約の対象は機能要件に限りません。性能、可用性、セキュリティ、互換性など、非機能要件であっても外部から検証可能なものは契約として定義します。定義した契約には、対応する検証方法を紐付けます。

非機能要件のうち、統合テストまたはE2Eテストで検証可能なものは契約として定義します。それ以外は、本番相当環境や運用環境で追加の試験・監視を行い、別途検証します。

契約は統合テストまたはE2Eテストで必ず検証することを前提とします。

契約を事前に固め、その検証テストを継続的に実行することで、リファクタリング後も契約で定めた振る舞いが維持されていることを保証します。契約テストでは検出できず、運用上デグレが判明した場合は、ユーザーが当然と考えていた振る舞いが契約に含まれていなかったと捉え、不足していた契約を追加・強化します。

契約対象ごとの契約の例

外部接点は契約対象が外部と接する経路を、観測面はその接点を通じて確認する結果を表します。1つの外部接点に複数の観測面を持つことがあります。

契約対象 外部接点 観測面 契約 条件 保証する振る舞い 検証例
注文受付 注文API 注文登録レスポンス、保存された注文内容 注文を登録する 有効な商品と数量が指定されている 注文を登録し、注文IDを返す 返された注文IDで取得した商品と数量が、指定した商品と数量に一致する
注文受付 注文API エラーレスポンス、注文の未登録状態 不正な注文を拒否する 存在しない商品または不正な数量が指定されている 注文を登録せず、エラーを返す 注文が登録されず、所定のエラーが返される
注文照会 注文API 注文取得レスポンス 注文を取得する 存在する注文IDが指定されている 指定された注文の内容を返す 登録済みの注文内容と一致する
在庫確保 在庫確保バッチ 在庫数、注文の在庫確保状態 在庫を確保する 確定済みで在庫未確保の注文があり、必要な在庫が存在する 在庫を確保し、注文の状態を更新する 在庫数と注文の在庫確保状態が更新される
在庫確保 在庫確保バッチ 在庫数、在庫不足状態 在庫不足を記録する 注文に必要な在庫が不足している 在庫を確保せず、在庫不足を記録する 在庫数が変更されず、在庫不足状態が記録される
決済 決済API 決済結果レスポンス、保存された決済結果 決済を実行する 有効な注文と支払情報が指定されている 決済を実行し、結果を返す 決済結果が保存され、外部へ返される
決済 決済API 決済結果レスポンス、決済記録 決済の重複を防止する 決済済みの注文に対して再度決済が要求される 新しい決済を実行せず、既存の決済結果を返す 決済が重複せず、既存の結果が返される

契約対象ごとの契約もAIと対話しながら詳細に詰めていきましょう。検証についてしっかり詰めるのが重要です。

3. 内部設計

DDDによる業務分析で識別した業務境界を基に、アプリケーション内部を業務モジュールに分割します。

各モジュールは、他のモジュールに提供する機能だけを公開インターフェースとして公開し、内部実装を隠蔽します。また、モジュール間で許可する依存関係を明確に定義します。

モジュールの公開契約(インタフェースと振る舞い)を変更しない限り、特定のモジュールの内部実装を修正しても、原則として他のモジュールには影響しません。公開契約を変更する場合も、影響範囲をそのインターフェースに依存するモジュールへ限定できます。

これにより、変更によるデグレの発生範囲を小さくできます。また、いざとなればモジュール単位で内部実装を全面的に書き換えることも選択しやすくなり、メンテナンスの効率が高まります。

モジュール境界、公開範囲、依存方向、循環依存の禁止といった構造上の制約は、静的解析・アーキテクチャ検証ツール(dependency-cruiser, ArchUnitなど)によって継続的に検証します。これらの制約は具体的な内部実装を固定するものではなく、AIが内部実装を自由に変更できる範囲を定めるものです。

モジュール分割したディレクトリ構造例

src/
  ├── bootstrap/                 # アプリケーションの起動とモジュールの接続
  │
  └── modules/
      ├── order/                 # 注文管理モジュール
      │   ├── api/               # 他モジュールへ公開するインターフェース
      │   └── internal/          # 注文管理の内部実装
      │       ├── domain/
      │       ├── usecase/
      │       └── infrastructure/
      │
      ├── inventory/             # 在庫管理モジュール
      │   ├── api/               # 他モジュールへ公開するインターフェース
      │   └── internal/          # 在庫管理の内部実装
      │       ├── domain/
      │       ├── usecase/
      │       └── infrastructure/
      │
      └── payment/               # 決済管理モジュール
          ├── api/               # 他モジュールへ公開するインターフェース
          └── internal/          # 決済管理の内部実装
              ├── domain/
              ├── usecase/
              └── infrastructure/

この例では、各業務モジュールは、他のモジュールが利用できる api と、外部から直接参照できない internal に分けます。

モジュール間の依存は、相手の api だけに限定します。例えば注文管理が在庫管理を利用する場合、inventory/api には依存できますが、inventory/internal には依存できません。空の階層や不要なレイヤーは作らず、責務が必要になった場合だけ追加します。

Spring Boot なら Spring Modulith など、言語ごとに提供されているモジュール分割ツールを使用することで、より堅牢にモジュール構成を維持できます。

4. テスト

この記事では、テストを次のようにおおまかに分類します。

  • 単体テスト:関数やメソッド単位で動作を確認するテスト
  • 統合テスト:アプリケーション内部の処理をローカルコールで連携させ、動作を確認するテスト
  • E2Eテスト:外部のダミーサーバなどへ実際のプロトコルで接続し、外部接点から一連の振る舞いを確認するテスト

厳密な分類は技術や構成によって異なりますが、本稿ではこの分類を用います。

AI駆動開発では、単体テストの重要性は相対的に低くなります。

従来の開発では、単体テストは個々の実装単位が動くことを確認しながら、システム全体を段階的に組み上げるためにも利用されてきましたが、AIはコードの生成、ビルド、実行、エラーの解析と修正を繰り返すことで、単体テストがなくてもシステムが動作する状態まで到達させることが可能だからです。

しかし、実装が動くことと、契約どおりに正しく動くことは別で、重要なのは、 生成された実装が契約どおりに振る舞うかどうか になります。

そのため、AI駆動開発では、統合テスト、またはE2Eテストによって、契約の検証を網羅していくことが最優先となります。

ただし、単体テストをすべてなくすわけではありません。境界値、レアケース、多数の入力パターンなど、契約テストだけでは効率的に検証できない箇所に限定して利用していきます。

契約と契約テストの対応はしっかり管理しましょう。

5. 既存プロジェクトに組み込むには?

既存プロジェクトでも、DDDの分析を行い、契約対象を導き出して、契約を固定し、その検証テストを構築することは可能です。

しかし、大きな課題の一つは、すでにモジュール構成が出来上がっており、DDDによる業務分析に合わせて構造を全面的に変更することが実務上困難であることです。

ただし、内部構造は変更できなくとも、契約を固定することのメリットはあるため、既存の内部構造を維持したまま、契約駆動開発を段階的に適用することが可能です。

テスト移行方法

既存プロジェクトでは、契約に基づくテストを新たに構築し、既存テストと一定期間並行して実行します。

移行期間中は両方のテストを戦略的に管理し、契約テストに問題がなく、既存テストが担っていた保証を代替できると確認できた範囲から、段階的に既存テストを廃止します。

ただし、境界値、レアケース、局所的な不変条件、再発防止など、契約テストでは効率的に検証できない観点については、必要に応じて再設計して残します。

6. その他の〇〇駆動開発との比較

仕様駆動開発

ここでいう仕様駆動開発は、要求、設計、実装計画までを事前に構造化する開発全体のアプローチです。AIに与える情報を整理し、実装の指針を明確にできます。

一方、仕様書を実装の指針として与えるだけでは、AIが仕様どおりに実装したことを決定的に確認できません。仕様が検証可能な粒度になっていない場合、最終的な解釈がAIに委ねられ、実装結果も安定しません。

契約駆動開発は、この全体アプローチを置き換えるものではありません。外部から観測・検証可能な契約と、その検証に焦点を絞るため、仕様駆動開発の一部として組み込むことも、既存の開発プロセスに契約と契約テストだけを追加することもできます。

対象を契約に絞ることで、開発方法全体を変更せず、現場へ段階的に適用できます。AI駆動開発で重要なのは、AIの生成を決定的にすることではなく、 生成された実装の合否判定を決定的にする ことです。契約駆動は、AIの曖昧さを吸収しながら、外部から見た振る舞いがぶれないシステムを構築する基盤になります。

テスト駆動開発

AI駆動開発では、内部実装が想定どおりに動くことだけを確認するテストの重要性は低く、契約どおりに動作することを確認するテストが最も重要になります。

これを前提にすると、内部実装を対象とした単体テストや、契約とは直接関係のないコンポーネント接続テストを網羅的に記述する必要はなくなります。テスト駆動開発を採用する場合も、中心となるのは契約を検証するテストです。

外部契約をテストとして先に固定するのであれば、テスト駆動と契約駆動の本質的な目的は同一 になります。

ただし、テスト駆動では合否判定基準を特定の実装言語やテストフレームワークに依存したテストコードとして保持します。これに対して契約駆動では、実装言語から独立した契約を正本とし、テストコードをその検証手段として扱うため、AI駆動開発全体の方針として、より汎用性の高い選択になります。

まとめ

契約駆動開発では、DDDによって業務境界を定め、外部の振る舞いを契約として固定し、対応するテストによって継続的に検証します。

それにより、構造上の制約を守る限り内部実装を自由に変更できるようになり、AIによる変更を繰り返しても、エントロピーの増大をモジュール内に閉じ込め、システム全体への拡散を防ぐことで、継続的にメンテナンス可能なシステムを開発できます。

この記事では契約駆動開発の概念を中心に説明するため、具体的なDSLの構造までは扱いませんでした。

実際に運用する際は、この記事を codex などのAIエージェントに読ませ、プロジェクト固有の契約、検証内容、構造上の制約をDSLで管理するスキルを作成できます。

定義したDSLとスキルは、AIが参照する正本・運用資産としてプロジェクト内で継続的に管理します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?