仕様書駆動開発への違和感と、AI時代の「実装駆動開発」という選択肢

開発環境

はじめに — 仕様書駆動開発への違和感

AIを活用したソフトウェア開発が当たり前になりつつある今、「仕様書駆動開発」が注目を集めている。まず仕様書を書き、それをAIに渡してコードを生成する。一見合理的に見えるこの手法に、私は強い違和感を覚えている。

ウォーターフォールの再発明ではないか。

アジャイル開発がなぜ広まったのか思い出してほしい。仕様は変わるものだ。最初にすべてを見通すことなどできない。だからこそミニマムに作って動かしフィードバックを得て高速にPDCAを回す。この思想が現実のソフトウェア開発に適合したからこそアジャイルは主流になった。

仕様書駆動開発はこの歴史的教訓を無視しているように見える。

仕様書駆動の構造的問題 — 乖離は必然である

仕様書駆動開発の最大の問題は仕様とコードが乖離していくことにある。

最初のイテレーションで仕様書を書きコードを生成する。ここまではいい。しかし次のイテレーションで要件が変わったとき、まず仕様書を修正しその修正をコードに反映しなければならない。イテレーションを重ねるたびに双方を同期し続ける負担は増大していく。

そしてある時点でAIのコンテキストウィンドウに仕様書全体とコード全体を収めきれなくなる。部分的にしか参照できない状態で同期を保つのは人間にとってもAIにとっても極めて困難だ。仕様書には書いてあるがコードには反映されていない機能、コードには存在するが仕様書に記載のない振る舞いが必ず生まれる。

これは運用の怠慢ではなく構造的な必然だ。

発想の転換 — 仕様書は「設計図」ではなく「竣工図」

ここで考え方を逆転させる。

仕様書を事前に書いてコードを生成するのではなく、先に実装しコードから仕様書を事後的に生成する。仕様書を「設計図」ではなく「竣工図」として扱う発想だ。

この発想を支えるのはAIの能力の非対称性である。

自然言語 → コード: 曖昧さの解釈が入り、意図と異なるコードが生まれやすい

コード → 自然言語: コードは厳密であり正確な記述を起こせる

精度の高い変換方向を活かすならコードから仕様書を生成するほうが理にかなっている。

実装駆動開発のサイクル

具体的には以下のサイクルを回す。

“`

【イテレーション1】

要求 → 簡易な方針メモ → 実装 → コードから仕様書 v1 を生成

【イテレーション2】

仕様書 v1 + 追加要求 → 実装 → コードから仕様書 v2 を生成

【イテレーション N】

仕様書 v(N-1) + 追加要求 → 実装 → コードから仕様書 vN を生成

“`

重要なポイントが2つある。

1. Source of Truth は常にコード

仕様書はコードの「影」でありいつでも再生成できる。仕様書とコードの同期問題は原理的に発生しない。

2. 定常状態では入力の大部分が高精度な仕様書で構成される

2回目以降のイテレーションにおいてAIへの入力は「前回コードから生成された仕様書 + 差分の追加要求」になる。コードから生成された仕様書は精度が高く、曖昧さが入り込むのは「追加要求」の部分だけだ。

仕様書駆動では入力となる仕様書が人間の自然言語で書かれ続けるためイテレーションを重ねるほど曖昧さが蓄積する。実装駆動ではこの問題が構造的に回避される。

仕様書の二層構造 — Why と What の分離

実装駆動のサイクルを実運用に乗せるにあたり仕様書を二層に分ける。

Y層(Why / 要求仕様)

「何のために何を作るか」を記述するレイヤー。プロダクトのビジョン・機能要件・ビジネスロジックをユーザーストーリーやジョブレベルの粒度で記述する。抽象度が高いぶん変更頻度は低く安定的である。

W層(What / 詳細仕様)

「何をどう実装しているか」を記述するレイヤー。API仕様・データモデル・処理フローをコードから毎イテレーション自動生成する。常に現在の実装を正確に反映する。

この分離にはAIエージェントの役割に応じたコンテキスト最適化という実用的な意味がある。

| エージェントの役割 | 必要なコンテキスト |

|—|—|

| プロダクトマネージャー | Y層のみ |

| プロジェクトマネージャー | Y層 + W層 |

| 実装エージェント | Y層 + W層 + 関連コード |

AIのコンテキストウィンドウは有限だ。全員に全情報を渡すのではなく役割に応じて必要な情報だけを渡すことで精度と効率の両方を高められる。

Ports and Adapters — コード構造が仕様書になる

ここまで「仕様書をコードから生成する」と述べてきたが、さらに一歩進んで考えたい。そもそも仕様書を別途生成する必要があるのか?

Alistair Cockburnが提唱したPorts and Adapters(Hexagonal Architecture)はシステムの内側と外側をPortというインターフェースで分離するアーキテクチャパターンだ。Portはシステムが外部に対して「何ができるか」を宣言するインターフェースでありAdapterはそのインターフェースの具体的な実装である。

この構造を見て気づく。Portは仕様そのものではないか。

Portは「このシステムは何ができるか」をインターフェースとして厳密に定義する。依存関係は型で表現されIDEでコードジャンプすれば依存先に飛べる。Find All Referencesで呼び出し元を一覧できる。Portの変更はAdapterのコンパイルエラーとして即座に検出される。

つまりPorts and Adaptersのアーキテクチャを徹底すればコード構造自体が構造化された仕様書として機能する

コントラクトの具体例

ECサイトを例にとる。すべてのモジュールが以下の規約に従うとする。

modules/
  order/
    usecases.ts          ← Port: ユースケースのインターフェース定義
    usecases.impl.ts     ← Adapter: ユースケースの実装
    types.ts             ← このモジュールが扱う型定義
    repository.ts        ← Port: 外部依存のインターフェース
    repository.impl.ts   ← Adapter: 外部依存の実装

`usecases.ts` には実装を一切書かず純粋なPort(インターフェース定義)だけを置く。

// modules/order/usecases.ts — Port

import type { User } from '../user/types'
import type { CartItem, Order, OrderId } from './types'
import type { PaymentResult } from '../payment/types'

export interface OrderUseCases {
  /** カートの商品から注文を作成する */
  placeOrder(user: User, items: CartItem[]): Promise<Order>

  /** 注文をキャンセルし返金処理を開始する */
  cancelOrder(user: User, orderId: OrderId): Promise<PaymentResult>

  /** 注文の配送状況を取得する */
  trackDelivery(orderId: OrderId): Promise<DeliveryStatus>
}

この規約がもたらすもの

全モジュールの `usecases.ts`(Port)を集めればそれがそのままY層(要求仕様)になる。

`import` 文がモジュール間の依存関係を表現しIDEのコードジャンプで依存を追跡できる。上の例では `OrderUseCases` が `User`・`PaymentResult` をimportしていることから「注文はユーザー管理と決済に依存する」という仕様上の関係が型として明示されている。Port(インターフェース)を変更すればAdapter(実装)にコンパイルエラーが出る。仕様とコードの乖離をコンパイラが防ぐのだ。

AIにコンテキストを渡す際も `usecases.ts` と `types.ts` だけ渡せばY層として十分でありファイル名の規約が統一されているので振り分けは機械的にできる。

先ほどの二層構造をPorts and Adaptersの用語で再整理するとこうなる。

“`

Y層(要求仕様) = 全モジュールの Port(usecases.ts + types.ts)

W層(詳細仕様) = 全モジュールの Adapter(*.impl.ts)

“`

別の仕様記述言語もDSLも必要ない。Ports and Adaptersというアーキテクチャ規約を徹底することでTypeScript自体が仕様記述言語として機能する。

なぜ今まで仕様書が必要だったのか

逆に考えると仕様書を別途作成しなければならなかった根本原因はコード構造が不統一だったからだ。モジュールごとにユースケースの書き方がバラバラで「どこが仕様でどこが実装か」を人間が都度判断しなければならない状態では仕様の抽出は困難になる。

Ports and Adaptersの規約を全モジュールに徹底すれば「Portを読めば仕様がわかる」という一貫性が保証される。仕様書は不要になるのではなくコードの中に構造的に組み込まれる。

このアーキテクチャの制約とデメリット

ここまでPorts and Adaptersを軸にした実装駆動開発の利点を述べてきたが、このアーキテクチャが万能であるかのような印象は正しくない。実際に採用するにあたって認識すべき制約がある。

フレームワークとの衝突

現実のプロジェクトはフレームワークの規約の上に成り立っている。Next.jsのApp RouterはRoute Handlerやlayout/pageの配置に独自の構造を要求するしRailsにはMVCのディレクトリ構成がある。これらのフレームワーク規約とPorts and Adaptersのモジュール構造は素直に共存しないことが多い。

フレームワーク層はフレームワークの規約に従いつつビジネスロジック層にだけPorts and Adaptersを適用するといった適用範囲の切り分けが現実的な落とし所になる。

UI・フロントエンドへの適用の難しさ

Ports and Adaptersはバックエンドのビジネスロジックには自然にフィットするがフロントエンドのUI層にはそのまま適用しにくい。「この画面にはこの情報が表示される」「このボタンを押すとこの処理が走る」といったUI仕様はインターフェース定義だけでは表現しきれない。

コンポーネントのprops定義がPortに近い役割を果たす可能性はあるが、画面遷移・レイアウト・インタラクションの仕様まで含めるとPortの表現力では足りない。UIに関してはStorybookやデザインシステムの型定義などを組み合わせる必要がある。

横断的関心事の居場所

認証・認可・ロギング・トランザクション管理・エラーハンドリングといった横断的関心事は特定のモジュールの `usecases.ts` には収まらない。複数のモジュールにまたがるこれらの処理をPorts and Adaptersの枠組みだけで表現しようとすると不自然な設計になる。

middleware層やshared infrastructure層として別途整理しPortからは「前提条件」として参照する形が必要になるだろう。この部分はPorts and Adaptersの外側の設計判断であり「コード構造=仕様書」のモデルだけではカバーしきれない。

小規模プロジェクトへのオーバーエンジニアリング

PoC・プロトタイプ・小規模なツールなど寿命が短くモジュール数も少ないプロジェクトにPorts and Adaptersを導入するのは過剰だ。Port/Adapterの分離はコード量を増やし小さなプロジェクトでは抽象化のコストが利点を上回る。

このアーキテクチャが効果を発揮するのはモジュール数がある程度あり長期的に保守していくプロダクトである。プロジェクトの規模とライフサイクルに応じて導入を判断すべきだ。

規約の強制コスト

「全モジュールが同じ構造に従う」という規約は宣言するだけでは守られない。チームメンバーやAIエージェントが規約を逸脱した場合にそれを検出し是正する仕組みが必要になる。

リンタールール・アーキテクチャテスト・CIでの構造チェックなど規約を機械的に強制する仕組みへの投資が前提となる。この仕組みがなければ規約はすぐに形骸化しPorts and Adaptersを基盤とした「コード構造=仕様書」のモデルは成立しなくなる。

既存コードベースへの導入障壁

既に動いているプロダクトにPorts and Adaptersを後から導入するのは大きなリファクタリングを伴う。全モジュールを一度に移行するのは現実的ではなくモジュール単位で段階的に移行することになるが、移行期間中は新旧の構造が混在する。

この混在状態ではPort群を集めて仕様書とするモデルが部分的にしか機能しない。移行計画を立て優先度の高いモジュールから着手していく忍耐が必要だ。

つまり — 万能ではないが方向は正しい

これらの制約はPorts and Adapters固有の問題というよりもあらゆるアーキテクチャ規約を統一的に適用しようとしたときに生じる普遍的な問題だ。「適用範囲を見極める」「フレームワーク層とビジネスロジック層を分ける」「規約の強制を仕組み化する」という対処は必要だが、実装駆動開発の根本思想 — コードをSource of Truthとし仕様書はそこから導出する — はこれらの制約があっても揺るがない。

Ports and Adaptersは実装駆動開発を実現するための有力な手段の一つであり唯一の手段ではない。プロジェクトの特性に応じて適用範囲と粒度を調整しながらコードが仕様の源泉であるという原則を守ることが本質だ。

まとめ

この開発モデルの核心は3つに集約される。

1. 仕様書は保守するものではなくコードから導出されるもの — あるいはPorts and Adaptersを徹底すればコード構造自体が仕様書になる

2. AIの得意な方向(コード → 自然言語)を活かし苦手な方向(自然言語 → コード)への依存を最小化する

3. Ports and Adaptersの規約を統一することで仕様の構造化・依存追跡・整合性検証をプログラミング言語の仕組みに委ねる

仕様書駆動開発は「正しい仕様書を書けば正しいコードが生まれる」という前提に立っている。しかし正しい仕様書を最初から書くことはできないし仕様とコードを同期し続けるコストはイテレーションとともに増大する。

実装駆動開発はこの前提を捨てる。コードが唯一の真実であり仕様書はその影に過ぎない。影は本体が動けば自然についてくる。本体と影を別々に管理しようとするから乖離が生まれるのだ。

そしてPorts and Adaptersが教えてくれるのは影を別に作る必要すらないということだ。Portという形で仕様はコードの中に最初から存在している。あとはそれを一貫した規約で書くだけでいい。

もちろんこのアーキテクチャは万能ではない。フレームワークとの衝突やUI層への適用の難しさといった現実的な制約はある。だがそれは適用範囲の問題であって思想の問題ではない。コードをSource of Truthとし仕様はそこから導出するという方向性は、AIの特性を踏まえれば自然な帰結だと考えている。

コメント