「仕様」の正体:Requirements・Contract・Structureによるソフトウェアの理解
導入:「仕様」という言葉の曖昧さ
ソフトウェア開発において「仕様」「機能」という言葉は頻繁に使われるが、その意味は驚くほど曖昧だ。
「この機能の仕様は?」と聞かれたとき、何を答えるべきだろうか。ユーザーが何を期待しているかの話なのか、APIのリクエスト・レスポンスの形式の話なのか、内部のデータモデルとロジックの話なのか——同じ「仕様」という言葉が、まったく異なる概念を指して使われている。
この曖昧さは単なる言葉の問題ではない。異なるレイヤーの概念を無区別に扱うことが、ソフトウェアについての思考そのものを妨げている。
本稿では、「仕様」「機能」と呼ばれるものを Requirements・Contract・Structure/Behavior という枠組みで分解し、それぞれの関係を明確にすることを試みる。
3つのレイヤー
Requirements(要求)
Requirementsとは、ステークホルダーが世界に対して望む状態・結果だ。
- 「商品を検索できる」
- 「注文後にメールが届く」
- 「月末に請求書が発行される」
これらはソフトウェアの外側にある。ユーザーや事業者がどのような結果を得たいかという期待であり、ソフトウェアの構造や実装とは独立して存在する。
要求工学における Zave-Jacksonの枠組み では、Requirementsは環境(世界)に対する望ましい性質として定義される。ソフトウェアはその性質を実現する手段にすぎない。
Structure と Behavior(構造と振る舞い)
ソフトウェアの実体は、構造と振る舞いという二つの側面から構成される。
Structure(構造) とは、概念間の結合・依存関係・境界から生まれる制約の総体だ。モジュール間の依存関係、概念間の不変条件(「出荷済み注文には支払い日時が必ず存在する」)、技術的制約がこれに含まれる。
Behavior(振る舞い) とは、特定の入力に対する出力、あるいは操作による状態変化だ。
そして決定的に重要なのが、Structure が Behavior を制約する という関係だ。構造が状態空間を限定すれば、その中で起こりうる振る舞いのパターンも限定される。良い構造は振る舞いの実装を単純にし、検証すべきケースを減らす。
Contract(契約)
Contract とは、モジュールの境界(Interface)における Structure と Behavior の両面の約束だ。
Contractには二つの側面がある:
- Structure的側面:インターフェースの形の定義——引数の型、戻り値の型、許容される状態。型システムが静的に保証する
- Behavior的側面:特定の入力に対する具体的な出力、状態遷移の正しさ。テストが動的に検証する
Contractはモジュールの境界に存在し、内部のImplementationを隠蔽しつつ、外部に対して何を約束するかを宣言する。
3層の依存関係
因果の方向
Implementation(Structure + Behavior)
↓ 決定する
Contract(境界での約束)
↓ 満たすか?
Requirements(世界に対する期待)
- Structure が Behavior を制約する
- Structure と Behavior が Contract を実現する
- Contract が(Domain Knowledgeと合わさって)Requirements を達成する
これは Zave-Jacksonの枠組み の定式化 S ∧ D ⊨ R に対応する。S(Specification/Contract)と D(Domain Knowledge)が R(Requirements)を論理的に含意する。
プロセスの方向は逆
開発プロセスでは Requirements → Contract → Implementation の順で考えることが多い。しかしこれは思考の順序であって因果の方向ではない。
実際の設計は構造と振る舞いの往復運動であり、コードに落とす段階では「構造を先に、振る舞いを後に」が有効だ。
Contract の推論可能性
ここで、Contractの質について重要な基準がある。良いContractとは、I/Fから内部のStructureが推察可能であることだ。
推論可能性がなぜ重要か
ソフトウェアの利用者(他のモジュールの開発者、APIの呼び出し側)は、Contractを通じて内部のStructureを推察し、「この状態ならこう相互作用できるはず」と推論する。その推論に基づいて利用する。
- 推察通りに動く → 利用者は「期待通りだ」と感じる
- 期待通りの動作がRequirementsと合致する → 「正しい」と判断される
つまり、「正しいソフトウェア」に到達する経路は:
Contract が Structure を推察可能にする
→ 推察に基づいて利用する
→ 推察通りに動く(= 期待通り)
→ それが Requirements と合致する(= 正しい)
Contractの推論可能性が、この経路全体の起点になっている。
推論可能性と型の表現力
型システムはContractの推論可能性を高める強力な手段だ。例えば判別共用体で状態を表現すれば、I/Fの型シグネチャを見ただけで状態遷移の構造が読み取れる。
// 推論しやすい — 型がStructureを伝えている
type Order =
| { status: "draft" }
| { status: "paid"; paidAt: Date }
| { status: "shipped"; paidAt: Date; shippedAt: Date };
// 推論しにくい — Structureが隠れている
type Order = {
status: string;
paidAt: Date | null;
shippedAt: Date | null;
};前者は型を見ただけで「出荷済みには支払い日時が必ずある」というStructureの制約が推察できる。後者ではドキュメントやソースコードを読まなければ分からない。
ドメインの認知構造がコードに直接反映されている状態では、ドメイン知識を持つ観測者がI/Fを見たとき、自分のドメイン理解からStructureを正しく推察できる。だからFBループが速い。
「仕様と違う」の切り分け
この枠組みを使えば、「仕様と違う」「バグだ」という曖昧な報告を構造的に切り分けられる。
| 症状 | 実際に壊れている箇所 | 対処 |
|---|---|---|
| 推察通りに動かない | Contract が Structure を正しく伝えていない | I/Fの設計を改善し、型の表現力を上げる |
| 推察通りだが期待と違う | Requirements と Contract が乖離している | 要件定義とI/F設計を見直す |
| そもそも推察できない | Contract の推論可能性が低い | I/Fの再設計、型による状態表現の導入 |
| 推察通りかつ期待通りだが正しくない | Requirements 自体が誤っている | ステークホルダーとの再確認 |
「仕様」という一語では、これらの区別がつかない。Requirements・Contract・Structure/Behavior という語彙を持つことで、問題の所在を正確に特定できるようになる。
「機能」の多義性も同様
「機能」という言葉も同じ構造の曖昧さを持っている。
| レイヤー | より正確な語 | 意味 |
|---|---|---|
| Requirements | Capability | ユーザーが得たい能力 |
| Contract | Protocol | その能力を提供するI/Fの約束 |
| Implementation | Mechanism | その約束を実現する内部構造と振る舞い |
「この機能を追加してください」が Capability の追加なのか、Protocol の変更なのか、Mechanism の実装なのかで、やるべきことはまったく異なる。
結論
「仕様」「機能」という日常語は、Requirements・Contract・Structure/Behavior という異なるレイヤーの概念を無区別に指している。
- Requirements はソフトウェアの外にある世界への期待
- Contract は境界における Structure と Behavior の約束であり、型とテストがそれぞれの側面を保証する
- Structure が Behavior を制約し、両者が Contract を実現する
そして、良いContractの条件は推論可能性だ。I/Fから内部のStructureが推察でき、推察通りに動き、それがRequirementsと合致するとき、人はソフトウェアが「正しく動いている」と感じる。
この語彙を持つことで、「仕様と違う」という曖昧な問題報告を、どのレイヤーの何が壊れているのかという構造的な分析に変換できる。ソフトウェアについて正確に思考し、正確に伝えるための道具として、この枠組みが役立つことを願っている。
