はじめに:生成AI時代の「構造化能力」の再定義
コード生成の容易さと構造維持の困難さ
生成AIがコード生成の障壁を下げることで、機能を実装する速度は向上することが多いです。しかし、プロジェクトを数ヶ月単位で維持できるかどうかは、生成したコードを構造化して置き場所を決める能力に依存します。多くのチームで観察されるのは、初めは小さく動く実装が、機能追加を重ねるごとにファイルや関数が複数の責任を背負い始め、最終的に修正箇所を特定しにくい状態になるパターンです。
この罠は、AIが賢くコードを書くかどうかよりも、人間側が構造を言葉にしていない場合に生じやすい。変更のたびに「どこまで直せば影響しないか」を推測で判断する開発は、バグの再現性とレビューの容易性を同時に失います。Building With AI When You Don't Know Architecture: A Survival Guide で扱われている問題意識は、まさにこの点に一致します。
本記事の焦点:設計意図を言葉にするプロセス
本記事では、技術選定や環境構築の説明は最小限にし、アーキテクチャの専門知識が乏しい状態でも適用できる実務パターンを扱います。ドメイン駆動設計(DDD)の考え方を過度に一般化せず、UI・ビジネスロジック・データの分離という最小単位に落とし込んだ形です。重要なのは、AIに「この層はこれだけを担当する」という境界を提示し、生成結果をその境界に対して検証できる状態にすることです。
つまり、焦点は「何を導入するか」ではなく、「どうやって理解可能な構造をAIと共に対象化し、維持するか」のプロセスにあります。読者が最終的に得られるのは、次の機能追加時にどのファイルに触れるべきか、どの関数が変更の影響を受けるかを判断するための材料です。
前提:なぜAI時代こそ「単純な構造」が必要なのか
複雑さへの誘惑と保守コスト
AIによるコード生成速度が上がると、設計品質の低下を加速させるリスクが同時に大きくなります。問題に対して「今すぐ動く複雑な解決策」を生成させると、短期間の成果には見えますが、後から同じ領域を修正するたびに追加の理解コストが発生します。既存のパターンに従い、一貫性を持たせることが、最もコストが低いアプローチになるのはこのためです。
複雑な実装は、レビュー時に「なぜこの構造なのか」を説明できなくなる傾向があります。説明できない構造は、次に触る人が推測で変更する入口になり、結果としてバグの温床になります。単純さを選ぶのは美学ではなく、変更影響範囲を小さく保つための設計判断です。
単一の真実の源という最小限の約束
もう一つの前提は、データの単一の真実の源(Source of Truth)を確立することです。同じデータが複数箇所に複製されると、更新タイミングのズレが原因で不整合が発生しやすくなります。長期的な技術負債の最小化には、どのデータがどこで正とされるかを明示し、変更経路を一本にする設計が有効です。
アーキテクチャ知識が不足している状態では、直感的に分かりやすい単純さが、チーム全体の合意形成とバグ防止の防御壁になります。「この関数はデータを変更しない」「このファイルは画面描画だけを担当する」といった制約を言葉にできるほど、AIの出力を制御しやすくなります。
設計指針1:責任の明確な分離(UI・ロジック・データ)
別ファイル分離と単一責任
AIにコードを生成させる際、UI、ビジネスロジック、データ層を物理的に別ファイルに分離するルールを徹底します。各ファイルや関数は一つの明確な責任のみを持ち、DBアクセスと画面描画が混在したような実装を避けると、レビューや修正時の影響範囲を特定しやすくなります。
例えば、注文処理の機能を追加する場合でも、表示担当のUIファイル、業務ルールを担当するロジックファイル、保存と取得を担当するデータファイルに分けます。この分離は、AIが生成したコードのレビュー時に「変更がどの層に閉じているか」を確認するための必須条件です。
src/
ui/
OrderList.tsx
logic/
OrderService.ts
data/
OrderRepository.ts
この構成では、画面の表示形式を変更する場合にui/内を、計算ルールを変更する場合にlogic/内を、保存先を変更する場合にdata/内を参照すればよく、変更対象の推測が不要になります。
外部SDK統合とアダプター層
ReactやVueなどのUIライブラリ、検索や外部サービス連携のSDKを統合する場合も、同じ原則を適用します。外部SDKの呼び出しをUI層に直接書くと、UIがインフラの詳細に依存し、置き換え時に影響範囲が広がります。そこで、SDKを呼ぶ責任をアダプター層に閉じ込め、UI層とビジネスロジック層はインターフェース経由で使用する形にします。
この設計は、依存関係の方向を「UI → ロジック → データ」に固定するものです。データ層がUIを変更したり、ロジック層が画面描画に直接触れたりすると、循環依存や重複した責任が生じやすくなります。
flowchart LR
A["UI層"] --> B["ビジネスロジック層"]
B --> C["データ層"]
A --> D["画面描画と入力収集"]
B --> E["業務ルールの適用"]
C --> F["データ保存と取得"]
設計指針2:事前定義による「プロンプト依存」の排除
AIに「この機能を実装して」と指示すると、AIは文脈から最適な解決策を推測してコードを生成します。しかし、その推測は毎回異なる可能性があります。同じ要件を別のセッションで再度尋ねた際に、ファイル構成や関数の粒度が異なり、結果としてプロジェクト内に二つの「正解」が混在する状態になります。これが「プロンプト依存」の罠です。
この問題を回避するためのアプローチは、コード生成の前にアプリの主要なパーツとそれぞれの責任を人間側で定義し、それをAIとの対話における唯一の基準として提示することです。この定義書は、形式のあるアーキテクチャ文書である必要はありません。パーツの名前、各パーツが担う責任、パーツ間の依存方向、変更時の制約条件を数行で記述したメモであっても構いません。重要なのは、AIにコードを生成させるたびにこの定義書をコンテキストとして渡すことで、出力の一貫性を担保する点にあります。
さらに、パーツを一つずつ構築するアプローチを取ることで、部分的な失敗が全体に波及するリスクを低減できます。例えば、データ層のインターフェースを先に定義し、AIにその実装を生成させた後、ビジネスロジック層のインターフェースを定義し、その実装を生成させるという順序です。各層が独立して検証できるため、バグの特定が容易になります。
アーキテクチャの専門知識がない状態でも、この「事前定義」のプロセス自体が設計意図を言語化する訓練になります。「このパーツは何かを担うのか」と自問する行為が、設計判断の根拠を明確にするのです。
| AIへの指示内容 | 人間側で事前に定義すべき要素 |
|---|---|
| 「ユーザー登録機能を実装して」 | 登録処理の責任範囲(バリデーション・保存・通知の分離) |
| 「検索APIを統合して」 | 外部SDKの呼び出しをどの層に置くか、インターフェースの形状 |
| 「この関数を修正して」 | 変更してはならない呼び出し元、影響範囲の境界線 |
| 「テストを書いて」 | テスト対象の責任範囲、モックすべき外部依存の特定 |
この表の左側は、多くのチームがAIに投げかける典型的な指示です。右側は、その指示を投げる前に人間が確定させておくべき事項です。左側だけの指示はAIの推測に委ねるのと同じであり、右側を定義することで、AIの出力が期待する範囲に収まるようになります。
学習プロセス:AIを「設計者」として活用する対話パターン
AIが生成したコードをそのまま受け入れるのではなく、その構造の理由や影響範囲をAIに説明させるプロンプトを意図的に挟むことで、開発者の役割が「コードの受け手」から「構造の理解者」へと移行します。これは単なる確認作業ではなく、設計判断の根拠を言語化させることで、自分自身の理解のギャップを可視化するプロセスです。
具体的なプロンプトの例を挙げます。「この関数がデータ層に直接アクセスしている理由を説明してください。UIの詳細を知らなくても同じ結果が得られるように書き換え可能か?」といった問いです。AIが「UIのコンポーネント名を参照しているため直接アクセスしています」と答えた場合、それは結合度が高いことを示しており、設計上の改善点として扱えます。逆に「この関数は純粋な業務ロジックであり、外部依存を持ちません」と答えた場合、その関数はテストしやすく、安全に再利用できる状態であると言えます。
ここで重要な判断基準があります。AIの説明が矛盾していたり、前後で整合性が取れない場合、それは設計上の欠陥かプロンプトの不備のいずれかを示しています。例えば、「この関数はUIを知らない」と説明した直後に「コンポーネントのpropsを参照している」と述べた場合、関数の責任が曖昧であることを意味します。この矛盾を放置すると、将来的にその関数を変更した際に予期しない影響が波及します。
この対話プロセスは、チーム内の技術共有にも直結します。AIに説明させた内容と、それに対する自分の判断を記録しておけば、後から同じ設計判断を説明する必要がなくなります。また、チームメンバーが同じ対話を行うことで、設計意図の共有が自然に行われます。
sequenceDiagram
participant D as 開発者
participant AI as AIエージェント
participant R as 設計定義書
D->>R: 事前定義を確認
D->>AI: 「この構造の理由と影響範囲を説明して」
AI->>D: 設計判断の根拠を説明
D->>D: 説明に矛盾がないか確認
alt 矛盾がある
D->>AI: 「この矛盾を解消する修正案を」
AI->>D: 修正案を提示
D->>R: 設計定義書を更新
else 整合的
D->>D: 設計意図を記録
end
このシーケンスの核心は、AIを「正解を返す機械」ではなく「設計判断を説明するパートナー」として位置づける点にあります。説明を求めた結果、自分の理解が深まれば学習効果があり、矛盾が発見されれば設計の改善点が見つかります。どちらの場合も、システムはより保守可能な方向に進化します。
実装パターン:ドメイン駆動設計(DDD)のミニマル適用
ドメイン駆動設計(DDD)のフルスタックな導入は、中小規模のプロジェクトには過剰である可能性があります。しかし、DDDの核となる概念である「エンティティ」と「値オブジェクト」の区別、そして「ドメインロジックを純粋なコードとして分離する」原則のみを取り入れることで、AI生成コードの結合度を下げられる場合があります。
エンティティはIDによって同一性を持つオブジェクトであり、値オブジェクトは値によって同一性を決定される不変のオブジェクトです。この区別をAIに理解させることで、「このオブジェクトは変更可能か」「コピーして渡しても問題ないか」といった判断が明確になることがあります。例えば、ユーザーのメールアドレスは値オブジェクトとして不変に扱い、ユーザー全体はIDを持つエンティティとして扱う、といった形です。
// 値オブジェクト:不変で、値による同一性
class EmailAddress {
private readonly value: string;
constructor(value: string) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
throw new Error(`Invalid email: ${value}`);
}
this.value = value;
}
getValue(): string {
return this.value;
}
}
// エンティティ:IDによる同一性
class User {
constructor(
private readonly id: string,
private email: EmailAddress
) {}
changeEmail(newEmail: EmailAddress): void {
this.email = newEmail;
}
}
このコードのポイントは、EmailAddressがバリデーションを内包し、Userが状態遷移のルール(changeEmail)を持つことです。UI層やデータ層はこれらのクラスを直接操作せず、公開されたメソッド経由でしか状態を変更できません。これにより、「メールアドレスが空になる」ような不正な状態を防ぐことができます。
ドメインロジックとデータ層の間にリポジトリパターンを挟むことで、ドメインモデルがインフラストラクチャの詳細に依存しないようにします。
// リポジトリのインターフェース(ドメイン層)
interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
}
// 実装(インフラストラクチャ層)
class DatabaseUserRepository implements UserRepository {
constructor(private db: any) {} // 実際のDBクライアントに置き換えてください
async findById(id: string): Promise<User | null> {
const row = await this.db.query(
'SELECT id, email FROM users WHERE id = ?', [id]
);
if (!row) return null;
return new User(row.id, new EmailAddress(row.email));
}
async save(user: User): Promise<void> {
// 実装省略
}
}
AIにコードを生成させる際に、「この関数はUIの詳細を一切知らない」という制約をプロンプトに含めることで、結合度を下げられる場合があります。具体的には、「この関数はReactやVueのコンポーネント名、CSSクラス名、DOM操作を参照してはならない」と明示します。この制約がある場合、AIは純粋なロジックのみを出力する傾向があり、出力の品質が安定することがあります。
このパターンは、AIコーディングエージェントとの連携においても明確な境界線を提供します。エージェントがファイル単位でコードを生成・修正する際、「このファイルはドメイン層であり、インフラストラクチャのimportを含まない」というルールを提示すれば、エージェントの出力が層の境界を越えるリスクを低減できる可能性があります。複雑な設計フレームワークを構築するのではなく、こうした最小限の制約をAIとの対話に組み込むことが、アーキテクチャ知識が不足している状態でも実行可能なアプローチの一つです。
AIエージェント連携における注意点とトレードオフ
Claude CodeやCursorなどのAIコーディングエージェントは、ファイル単位でのコード生成・修正を自律的に行えるため、生産性の向上に寄与することがあります。しかし、これらのエージェントには構造的なリスクが内在しています。一つ目は文脈の消失です。エージェントが複数のファイルを横断して修正を行う際、前回の対話で合意した設計意図が薄れ、一貫性を欠く出力が生成されやすくなります。二つ目は過剰な最適化です。エージェントが「より良い実装」を自ら判断して既存コードを差し替える場合、意図しない挙動の変化や依存関係の破綻を引き起こす可能性があります。
ツールを統合する際の判断基準として、「複雑さが増すほど単純な構造を選ぶ」という原則が有効な場合があります。具体的には、Algoliaの検索インフラをReactやVueのSDKを通じて統合する場合、検索ロジックをUI層に直接埋め込むのではなく、前述のアダプター層を挟むことで、検索エンジンのAPI変更がUIに波及するリスクを閉じ込められる可能性があります。同様に、Algolia Assistのように既存のインデックスに基づきベクトルDBなしでAIアシスタントやQ&A機能を実装できるサービスは、導入コストを大幅に下げる利点があります。一方で、検索アルゴリズムのチューニングや応答の制御を完全に自前で行えないというカスタマイズ性の制約が伴います。このトレードオフは、自社のドメインが「標準的な検索・応答で十分か」を判断する基準になります。
AIエージェントにAlgolia CLIやスキルをインストールする場合も、その依存関係が既存のプロジェクト構造に与える影響を事前に評価すべきです。エージェントが生成するコードが、既存の層の境界を越えるimportを生じないか、データ層の責務を侵していないかを、生成直後に確認する習慣を設けることで、エージェントの自律性が設計の崩壊に直結するリスクを抑制できる可能性があります。
| ツール・サービス | 利点 | 欠点・制約 | 適用ケース |
|---|---|---|---|
| Claude Code / Cursor | ファイル単位の自律的生成・修正 | 文脈消失、過剰最適化のリスク | 層の境界をプロンプトで明示できる場合 |
| Algolia SDK(React/Vue) | 検索機能をUIに素早く統合可能 | SDK更新時の破壊的変更に注意 | アダプター層を挟んで依存を閉じ込められる場合 |
| Algolia Assist | ベクトルDB不要でAIアシスタントを実装可能 | 応答制御のカスタマイズ性が限定的 | 標準的なQ&Aで十分なドメイン |
落とし穴:データの一貫性と同期ズレを防ぐ
AIが生成するコードには、データの複製や並列更新を容易にしてしまう傾向があります。例えば、UI層でローカル状態を保持しつつ、データ層でも同じ値を保持する二重管理が生成されることがあります。初期段階では動作に問題がなくても、更新経路が二つ以上存在すると、ある時点から両者の値が一致しなくなり、いわゆる「同期ズレ」が発生します。この問題は、アーキテクチャ知識が不足している状態では特に検出が難しく、ユーザー報告で初めて判明するケースがあります。
対策の核心は、データを単一の真実の源に保持し、他の層からは参照のみ許可するか、明示的な更新メソッド経由でのみ変更可能にするというルールを厳格に適用することです。このルールをプロンプト制約として定義する際は、「このデータは repository.ts の update() メソッド経由でしか変更してはならない。UI層でのローカルコピーの作成は禁止」といった形で、禁止事項を明示的に記述します。AIは肯定形の指示よりも否定形の制約に対して出力を抑制しやすい傾向があるため、この書き方が効果的な場合があります。
テストコードの生成時にも、データの状態遷移が正しく表現されているかを確認するプロセスを設けます。具体的には、「更新前」「更新中」「更新完了」の各状態で、単一の真実の源の値が期待される値と一致するかをアサートするテストをAIに生成させます。このテストが失敗する場合、生成コードに複製や並列更新の痕跡があることを示唆しており、設計上の欠陥を早期に検出できる可能性があります。
stateDiagram-v2
[*] --> 整合状態
整合状態 --> 更新中: 単一経路での書き込み
更新中 --> 整合状態: 更新完了・値が一致
整合状態 --> 破綻: 並列更新・複製による分岐
破綻 --> 整合状態: 手動修正・同期処理
上図の「破綻」状態に遷移する経路を、プロンプト制約とテストで封じることが、データ一貫性を維持する実務的な手段の一つです。状態遷移が「整合状態 → 更新中 → 整合状態」の閉ループに収まっている限り、同期ズレは発生しにくくなります。
運用:コミュニティの知恵を活用した設計検証
設計判断の質を高めるには、自社内の経験だけに頼らず、外部のコミュニティから知見を得ることが有効な場合があります。Major League Hacking(MLH)は多くの開発者が参加するハッカソンコミュニティであり、短期間でプロトタイプを構築する過程で生み出された構造設計の事例が多数存在します。こうした事例は、時間制約下で「何を残し、何を切り捨てたか」が明確になっているため、設計の優先順位付けの参考になることがあります。
ただし、ハッカソン事例をそのまま自社に適用することは注意が必要です。事例の背景にある制約条件(チーム規模、技術スタック、期限)は自社と異なる場合がほとんどであり、表面的な構造を模倣しても意図した効果が得られない可能性があります。重要なのは、その構造を選んだ「理由」を読み解くことです。例えば、あるプロジェクトがモノリシックな構成を選んだ場合、それは「開発者1人・1週間の制約下で最も保守可能な選択だった」という背景があるかもしれません。この理由を抽出し、自社の制約条件に当てはめて再評価するプロセスを挟むことで、コミュニティの知見を実用的な設計判断に転換できる可能性があります。
また、MLHがDEV(dev.to)を買収し、Google CloudのGeminiと提携してAIネイティブなエンジニアリング教育を推進しているという動きは、AI時代の設計手法がコミュニティレベルで体系化されつつあることを示しています。こうしたリソースを活用しつつも、自社のドメインに適用した場合のトレードオフを常に問う姿勢が不可欠です。AIの進化に伴い、アーキテクチャのベストプラクティス自体が変化するため、四半期ごとに「現在の構造は依然として最適か」を見直すプロセスを運用に組み込むことで、技術の陳腐化に対応しやすくなります。
まとめ:AI時代におけるアーキテクトの新たな役割
AIがコードを書く時代において、エンジニアに求められる能力は「コードを書く速度」から「構造化する能力」と「設計意図を定義する力」へと移行しています。AIはコード生成の障壁を取り除きましたが、プロジェクトを長期間維持できるかどうかは、人間が構造を定義し、一貫性を担保する能力にかかっています。
本記事で提示した三つの指針——UI・ロジック・データの責任分離、事前定義によるプロンプト依存の排除、AIへの説明を求める学習プロセス——は、それぞれ「構造を可視化する」「意図を固定化する」「理解を蓄積する」という異なる側面から、保守可能性を担保する可能性があります。これらは大規模なアーキテクチャフレームワークの導入を前提としておらず、ファイルの分離ルールやプロンプトの一文という最小単位で実行可能です。
複雑な解決策より単純で保守可能なものを選び、既存のパターンに一貫性を持たせるという原則は、ツール選定からデータ管理まで横断的に適用できます。AIを単なる生産性ツールではなく、設計パートナーとして位置づけることで、対話のたびに構造の理解が深まり、結果として高度な設計力が身につく可能性があります。アーキテクチャの専門知識が不足している状態から始まっていても、このプロセスを継続的に実践することで、システム全体を「理解可能な構造」として維持する力が養われていくでしょう。
関連記事
- AI時代の新規実装は「書かない」が正解:エンジニアの価値が判断力へシフトする理由
- AI時代の「中級エンジニアの消滅」と技術的負債:エージェント活用における品質保証の設計思考
- AI生成コードの信頼性を担保する「メンテナンスレシート」の考え方
参考
本記事は海外の技術トレンド「Building With AI When You Don't Know Architecture: A Survival Guide」(DEV)で話題のテーマをもとに、両儀システムソリューションズが独自に解説したものです。