この記事はこんな人におすすめ
- AIが生成したコードをどこまでレビューすべきか迷っている
- AIによって実装は速くなったが、レビュー負担も増えたと感じている
- AI時代に設計やテストを学ぶ意味を考えたい
この記事で得られること
この記事では、次の考え方を整理します。
- 契約によって「正しい」の条件を先に決める
- 副作用と抽象化によって、内部実装を読む範囲を限定する
- 契約をテストへ変換し、AIの実装を検証する
中心となる考え方は、
AIが生成したコードをすべて同じ密度で読むのではなく、人間が先に「守るべき仕様」を設計し、それを外側から検証できる状態を作る
ことです。
AIが書いたコードを読まなくてよい、という話ではありません。設計とテストによって、人間のレビューを重要な判断へ集中させることを考えます。
※ 私が参加した外部勉強会で学んだ内容をまとめたものです。
AIがコードを書くほど、人間が読む量も増える?
AIを使えば、以前より短時間で多くのコードを生成できます。
しかし、そのすべてを人間が一行ずつ確認しなければ正しさを判断できないなら、次のように人間側がボトルネックになります。
AIが大量にコードを書く
↓
人間が大量にコードを読む
↓
レビューが追いつかなくなる
内部実装をすべて読まなければ振る舞いが分からない構造そのものを減らすために、この記事では次の4つを考えます。
- 契約による設計
- 副作用の隔離
- 抽象化
- 契約を基準にしたテスト
1. 契約による設計―「正しい」の条件を先に決める
契約による設計(Design by Contract)では、処理の仕様を代表的に次の3つで整理します。
事前条件
→ 呼び出す前に満たすべき条件
事後条件
→ 事前条件を満たして正常終了した場合に成立する条件
不変条件
→ 維持されるべき状態
例えば、数量を変更する処理があるとします。
単に「数量を変更する」だけでは、負数を許可するのか、最大値はいくつか、上限を超えたらどうするのかが分かりません。
そこで、次のように契約を定義します。
事前条件
→ 追加量は正の整数
→ 変更前の数量 + 追加量は20以下
事後条件
→ 正常終了後の数量 = 変更前の数量 + 追加量
不変条件
→ 数量は常に1〜20の整数
上限を超えた場合に例外を投げるか、失敗を表す結果を返すかも決めます。
こうすると、
正しく実装してください
ではなく、
何を満たせば正しいのか
を先に示せます。
「契約による設計で実装して」とAIへ伝えるだけでは、契約そのものをAIが推測することになります。
人間が、有効な入力、正常終了後の状態、変更してよいもの、維持すべき条件を具体化する必要があります。
契約はコメントだけでなく、型、バリデーション、DB制約、テストなど複数の場所で表現できます。
重要なのは、
契約をどこで表現し、どこで強制するのかを決めること
です。
2. 副作用を隔離する―戻り値以外に何が変わる?
例えば、次の関数があるとします。
function calculateTotal(
prices: number[]
): number {
prices.sort((a, b) => a - b);
return prices.reduce(
(total, price) => total + price,
0
);
}
呼び出し側から見ると、合計値を計算して返す関数に見えます。
しかし、内部ではsort()によって、引数として渡された配列の順序も変更しています。
つまり、この関数の実際の結果は次の2つです。
合計値を返す
+
引数の配列を並び替える
この副作用が外側から分からなければ、安全に利用するために内部実装まで読む必要があります。
そこで確認したいのが、
この関数を呼んだあと、戻り値以外に何が変わるのか?
という問いです。
引数を変更しないなら、必要に応じてコピーしてから操作します。そもそも合計の計算に並び替えが必要なのかも見直せます。
副作用そのものが悪いわけではありません。DBへの保存、HTTP通信、ログ出力など、アプリケーションには副作用が必要です。
重要なのは、副作用がどこにあるのか分かるようにすることです。
外側:入力取得・認証・DB・HTTP
↓
内側:業務ルールの計算
↓
外側:保存・通知・ログ
すべてを純粋関数にするのではなく、
副作用を隔離したうえで、可能な部分を純粋にする
と考えます。
3. 抽象化―内部実装を毎回読まなくても利用できる境界を作る
例えば、次のコードがあるとします。
const charge = CallCharge.create(
durationMinutes
);
利用側は「通話時間から通話料金を作る」という外側の契約を理解できれば、毎回内部の料金計算式まで追う必要はありません。
良い抽象化とは、コードを別のクラスへ移すことではなく、
外側の契約を基準に利用できる境界を作ること
です。
ただし、抽象化すれば自動的に信頼できるわけではありません。
導入時や変更時には内部実装もレビューし、契約が型・制約・テストなどで強制されていることを確認します。その境界が検証され、安定しているからこそ、利用側が毎回内部実装まで読み直さずに済みます。
また、細かくクラスを作ればよいわけでもありません。独自のルールや不変条件があり、概念として名前を付ける価値があるものを抽象化します。
4. 契約をテストへ変換する
契約を決めたら、それをテストの基準にできます。
例えば、Quantityの不変条件が「1〜20の整数」なら、次のケースを確認します。
| 入力 | 期待結果 |
|---|---|
1 |
成功 |
20 |
成功 |
0 |
失敗 |
21 |
失敗 |
1.5 |
失敗 |
重要なのはテストコードの書き方ではなく、契約とテストを対応させることです。
人間が契約を決める
↓
契約をテストへ変換する
↓
AIが実装する
↓
契約への適合を検証する
AIへ「正しいコードを書いて」と依頼するのではなく、人間が先に正しさの条件を決めます。
テストが通れば「信頼できるコード」なのか
テストが通っても、システム全体の正しさが証明されたわけではありません。
例えば、次の問題は残る可能性があります。
- 契約そのものに仕様漏れがある
- 認可条件が抜けている
- 並行処理やトランザクションを考慮していない
- 外部I/Oが失敗した場合の仕様がない
- 性能などの非機能要件を検証していない
テストは、すべての入力や状態を自動的に証明するものではありません。
極端に言えば、
間違った仕様を正確にテストすれば、その間違った仕様どおりのコードをAIが作ることもできる
ということです。
そのため、AI時代のレビューでは、
どこを機械的に検証でき、どこに人間の判断を集中させるべきか
を設計する必要があります。
AIへ実装を依頼する前に考えたいこと
実装前に、次のような問いを使えます。
- 有効な入力と無効な入力は何か
- 成功後に何が成立しているべきか
- 常に守るべき不変条件は何か
- 戻り値以外にどのような副作用があるか
- どこを型・制約・テストで検証するか
特に重要なのが、
このコードを毎回すべて読まなくても利用できるようにするには、外側から何が確認できればよいか?
という問いです。
「全部読まないと何が起きるか分からない」という状態を、設計によって減らします。
まとめ
AIによってコードを書く速度が上がるほど、
AIが実装する
↓
人間が全部同じ密度で読む
だけでは、レビューが追いつかなくなる可能性があります。
そこで、次の流れを作ります。
人間が守るべき仕様を決める
↓
事前条件・事後条件・不変条件にする
↓
副作用を隔離する
↓
意味のある単位へ抽象化する
↓
型・DB制約・テストとして表現する
↓
AIに実装させる
↓
契約への適合を検証する
↓
高リスクな部分へ人間のレビューを集中する
重要なのは、
AIに「正しいコード」を考えてもらうのではなく、人間が「何を満たせば正しいのか」を先に決める
ことです。
コードを一切読まなくてよくなるのではありません。
毎回すべてを同じ密度で読む必要性を減らし、人間の判断が必要な場所へレビューを集中させることがポイントです。
契約、副作用の隔離、抽象化、テストは、AIが出てくる前からある設計の基本です。
AIによって生成できるコードが増えたからこそ、その重要性が改めて高まっていると感じました。
