🌊

仕様書駆動開発における「境界線」の引き方 ~UIは自由に、ロジックは不変に~

に公開

はじめに

結局AI駆動で開発する時に困るのって、コード量が増えると整合性が取れなくなっていくことだと思います。
UIを変えようとして、新規チャットでLLMに指示を出すと、重複した関数を増やし始めたり、コンポーネントを独自実装したり、既存のコンポーネントを使ってくれなかったり、、、
そんなあるあるを解決するには、変えていいコードと変えたく無いコードの線引きとその境界線の弾き方が肝になるのではないでしょうか。

実際、開発してて思うのは、UIだけなら散らかったコードになってもなんとか修正できるが、ロジックとUIが依存したコードの量が増えると難しくなってくということです。逆に言うと、適切にコードが分離されてれば、UIかロジックのどちらかがめちゃくちゃになっても片方に原因があるので、原因が特定しやすく修正も比較的容易です。

今回はAI駆動開発において、スムーズに開発をするための手法の一環として、プロジェクト内にガードレールを引くようなイメージで、「境界線」というテーマで手法を探っていきます。

アーキテクチャによる「大きな境界線」

AIに「触ってほしくない部分」を守るための最も強力な方法は、アーキテクチャレベルでの分離です。

関心の分離(SoC)の原則

関心の分離(SoC)は、プログラムをそれぞれが異なる関心事(UI、ビジネスロジック、データアクセスなど)を扱う個別のセクションに分割するという、ソフトウェア設計の基本概念です。LLMによるコード生成時に、UIやロジック、データ取得といった異なる役割を一つのファイルにすべて詰め込んだ、巨大なコードを生成し、SoCに違反することがよくあります。

AI駆動開発でSoCを徹底することは、AIの作業範囲を意図的に狭め、変更の影響範囲を限定するのに有用です。

クリーンアーキテクチャ

クリーンアーキテクチャは、このSoCをさらに厳格に適用したものです。その最大の特徴は依存性のルールにあります。

  • 内側(Entities, Use Cases): アプリケーションの最も重要で変更頻度の低いビジネスロジックやドメインルール。
  • 外側(UI, DB, External Interfaces): 変更が頻繁に発生する可能性のあるUIやデータベース、外部APIなど。

依存性のルールは、外側のレイヤーは内側のレイヤーに依存しても良いが、内側のレイヤーは外側のレイヤーに絶対に依存してはならないというものです。

内側のレイヤー(ビジネスロジック)には一切触れず、外側のレイヤー(UIコンポーネント)だけを変更するように指示することで、AIはアーキテクチャの制約の中で安全にコードを生成できます。

コンポーネントベースアーキテクチャ(CBA)

特にUI開発において、コンポーネントベースアーキテクチャ(React, Vue, Angularなどで一般的)は非常に有効です。UIを独立し、再利用可能なコンポーネントに分割することで、AIへの指示をより具体的かつ限定的にすることができます。

作業単位をコンポーネントに限定することで、AIがUI全体を破壊したり、勝手な実装をしたりすることを防ぎます。AIは既存の部品(コンポーネント)を組み立てることに集中できるため、開発者は意図通りの変更を効率的に得られます。

AIに境界線を認識させる具体的な方法

1. ベースプロンプトの作成

最もシンプルかつ効果的な方法の一つです。

クリーンアーキテクチャが適用された(適用したい)プロジェクトで使えるプロンプト例

以下のプロンプトをAGENT.mdなりCLAUDE.mdなり自作したspec.mdなりに入れてみましょう:

# ディレクトリ構成
/src
├── /app/         # UI (プレゼンテーション層)
│   ├── /components/
│   └── /pages/
├── /core/        # ビジネスロジック (ドメイン層・ユースケース層) 
│   ├── /domain/
│   └── /usecases/
└── /infra/       # データ永続化・外部API連携 (インフラストラクチャ層)
    ├── /repositories/
    └── /api/

# プロジェクトアーキテクチャ
私たちのプロジェクトは、クリーンアーキテクチャと関心の分離(SoC)の原則に厳密に従っています。
依存性のルール(内側が外側に依存しない)を絶対に破らないでください。

- /src/app/ (プレゼンテーション層): あなたが主に作業する場所です。UIコンポーネントのみを配置します。
- /src/core/ (ドメイン層・ユースケース層): ビジネスロジックの核心です。このディレクトリ内のファイルは絶対に編集しないでください。
- /src/infra/ (インフラストラクチャ層): データベースや外部APIとの通信を担います。このディレクトリ内のファイルは絶対に編集しないでください。

# ルール
- プレゼンテーション層(`/src/app`)には、UIの表示とユーザーインタラクションに関するコードのみを記述してください。
- ビジネスロジックやデータフェッチのロジックをUIコンポーネント内に直接記述することは固く禁止します。
- データの取得や更新は、必ず`/src/core/usecases`から提供される関数(フック)のみを呼び出してください。
- 状態管理には〇〇(例: Recoil, Zustand)を使用してください。
- 既存のUIコンポーネント(例: `Button`, `Input`, `Card`)を積極的に再利用してください。

2. コーディング規則ライブラリによる制約

プロンプトによる指示は非常に有効ですが、AIがうっかりルールを破ってしまう可能性もゼロではありません。そこで、ESLintやPrettierといった静的解析ツールを導入し、ルールを機械的に強制する手法が効果を発揮します。

  • Prettier: コードのフォーマット(インデント、最大行数など)を自動で統一します。AIが生成したコードのスタイルがバラバラになるのを防ぎ、レビューの負担を減らします。
  • ESLint: コードの品質やアーキテクチャルールへの違反を検知します。これがAIに対する強力な「静的なガードレール」となります。

以下のような指示でコーディング規則を作ってもらいましょう:

私たちのプロジェクトの品質と保守性を高めるため、厳格なルールを適用したPrettierとESLintの設定ファイルを設計・実装してください。

# 目的
クリーンアーキテクチャの「依存性のルール」をESLintで機械的に強制し、アーキテクチャの崩壊を未然に防ぐ。

# ディレクトリ構造とアーキテクチャ
私たちのプロジェクトは、以下のディレクトリ構造でクリーンアーキテクチャを実装しています。
[ここに先ほどのベースのプロンプトを貼り付ける]

# ESLintの設定要件 (`.eslintrc.js`)
ディレクトリ構造とアーキテクチャに沿った「依存性のルール」をESLintで強制してください。
例えば、以下のようなルールをLintで強制したい。
`/src/core/` (ドメイン層) は、他のどの層 (`/src/app/`, `/src/infra/`) もimportしてはならない。

# Prettierとの連携
- ESLintとPrettierのルールが競合しないよう、正しく設定してください。
- Prettierのフォーマット違反をESLintのエラーとして報告するため、`eslint-plugin-prettier`を設定してください。

上記すべての要件を満たす、`.prettierrc.js`と`.eslintrc.js`の2つのファイルを生成してください。

また、先ほどのベースのプロンプトの# ルールの最後にこう付け加えるといいでしょう

- コード変更時には必ずlintを実行すること

テストコードによる振る舞いの固定

重要なビジネスロジックには、手厚いユニットテストや結合テストを用意します。

1. まずはテストをAIに作成してもらいます

既存のコードのテストをAIに書いてもらいます。

2. LLMへ指示を出す際には以下のプロンプトを入れます

以下のコード変更を行ってください。ただし、既存のテストがすべてパスすることが絶対条件です。

先ほどのベースのプロンプトの# ルールの最後に付け加えてもいいでしょう:

- コード変更時には必ず既存のテストがすべてパスすることを確認してください。

これは非常に強力なガードレールです。AIがロジックを意図せず破壊するような変更を加えた場合、テストが失敗するため、その変更が誤りであることを自動的に検知できます。AIはテストをパスするという制約の中で、最適な解を見つけようとします。
UIだけを変更したいのにロジックが変更されてしまった際に気づくことができます。

終わりに

書いてて思ったんですが、普通にソフトウェアエンジニアリングの知識だなーって感じでした。AI駆動開発してると過去の知識を忘れて雑に指示しがちですが、AIは雑な指示ではやってくれないので、こういった人間が積み上げてきた叡智をちゃんと取り入れながら開発していくのが大切ですね。

引用

Discussion