🪶

許可リストか、拒否リストか — AI 時代に再発見した『コード変更量最小化』の原則

に公開

はじめに

fixU Reboot プロジェクトで多拠点機能 (親会社が複数の子会社・店舗を束ねる構造) を AI ネイティブな進め方で実装していた、ある日のことでした。

親管理者向けの管理画面サイドメニューを「許可された 15 項目だけ表示する」要件があり、AI サブセッションが書いた実装の動作確認を dev02 環境で進めていたところ、15 項目のうち 4 項目が 404 / 500 エラー で開けないことが分かりました。許可リストに並べた path のうち 4 つが、Laravel route として存在していなかったり、依存データが空で落ちたりしていたのです。

ユーザー (筆者) からの指摘はこうでした。

今回は「15 項目だけ」を表示することが要件だが、実装上は 必要な 15 項目を作る のではなく、不要な項目を非表示にする というアプローチが正しい。そうすれば誤ったリンク先を指定するなどという問題はそもそも起こらない。

要件が満たせるのであれば変更するコード量は少ないほど美しい。これは人間時代でもそうでしたが、AI 時代でも変わらないと私は考えます。アンラーニングすべきことと、踏襲すべきことの見極めが重要ですね。

この指摘は、本記事の核そのものです。一文で蒸留すると、こうなります。

要件が満たせるならコード変更は少ないほど美しい — これは人間時代から不変の原則だが、AI が「綺麗そうな新規実装」を量産しがちな時代だからこそ、より意識的に踏襲する必要がある。

本記事は、「AI 時代のアンラーニング」シリーズの一本として、踏襲すべき古典的設計原則を再発見した記録です。姉妹記事との関係はこうなっています。

想定読者は、AI エージェントを業務に組み込んでいるエンジニア・テックリード・アーキテクトです。特に、「AI に新規実装を任せていたら、いつの間にか既存資産との整合が取れなくなっていた」という経験がある方、あるいは AI 時代の設計レビューでチェックすべき軸を整理したい方に読んでほしい内容です。


1. 同じ要件に対する二つの approach — 「許可リスト」か「拒否リスト」か

ある画面の特定ロールに対して「既存メニューのうち、この N 項目だけ表示する」という要件を考えます。実装の approach は大きく 2 通り。

  • 許可リスト (allow list): 触ってよい N 項目を新たに定義する (専用の Constants と認可 middleware を起こし、view 側もそれを参照する)
  • 拒否リスト (deny list): 既存メニューから「不要な項目」を引き算する (view 側の条件分岐 + 認可違反時の最小 redirect)

最初の AI による実装は前者を選びました。新しい Constants と middleware を起こし、責務が一つの class に閉じ込められた一見綺麗な構造になります。AI コードレビューでも「単一責任が明確」と肯定的に評価されやすい approach です。

しかしテスト環境にデプロイした時点で、許可リストの一部の項目で問題が顕在化しました。ある項目はそもそも route が存在せず 404、別の項目は依存データが空で 500 になる、といった整合の崩れです。

原因はシンプルでした。許可リストに並べた path が既存実装の route 一覧から派生したものではなく、別途用意された画面 mockup を「正の Source of Truth」とみなして写し取った結果、原本 (既存 route) と写し (新規 Constants) の間にズレが生じていたのです。

「許可リスト」は書いた瞬間に既存資産の 「真実の写し」 を作ります。原本と写しが同期しなくなった瞬間、写し側だけが破綻する。これは構造的な脆さで、レビューの厚さや個人の注意力で防ぎきれるものではありません。

§ 1 蒸留: 「許可リスト」approach は構造的に Source of Truth を二重化する。原本と写しが乖離した瞬間、写し側だけが破綻する。


2. 「拒否リスト」approach は引き算で要件を満たす

冒頭のユーザー指摘が示したのは、設計 approach そのものの転換でした。「拒否リスト」(deny list) で実装すれば、既存メニューが Source of Truth として残り、そこから「不要な項目」を引き算するだけで要件が満たせます。

転換後の view 側はこのような擬似コードになります。

@foreach($menu_items as $item)
    @if(!$is_parent_admin || !in_array($item->path, $hide_for_parent, true))
        <li><a href="{{ $item->path }}">{{ $item->label }}</a></li>
    @endif
@endforeach

middleware も認可違反時の redirect だけで済み、許可項目を別途列挙する必要がありません。

何が構造的に変わったかを言語化するとこうです。

  • 既存メニューが唯一の Source of Truth として残る (= 二重定義が構造的に発生不能)
  • 存在しない path はメニュー候補にそもそも挙がってこないため、「typo / 不在 path を表示してしまう」事故が発生不可
  • 新メニュー追加時は、明示的に hide リストに入れない限り自動的に表示される (同期忘れの罠がない)

「許可リスト」と「拒否リスト」は表面的には対称な仕組みに見えますが、Source of Truth との関係性が本質的に異なります。

§ 2 蒸留: 「拒否リスト」approach は既存資産を Source of Truth として残し、引き算で要件を満たす設計。divergence が構造的に発生不能なため、同期忘れによる事故が起きない。


3. 構造的比較と踏襲すべき 3 つの設計原則

両者を構造的に並べると、本質的な差は単なる行数ではなく Source of Truth の単一性にあることが分かります。

観点 許可リスト (allow list) 拒否リスト (deny list)
Source of Truth 既存実装 + 新規定義の 二重 既存実装 のみ単一
typo / 不在 項目 環境テストで初検出 構造的に発生不可
新項目追加時 双方を同期更新 自動的に追加 (hide list に入れない限り)
コード変更量 多い (新規 class / middleware / 登録の連鎖) 少ない (view 側条件分岐 + 最小 middleware)
AI コードレビューでの見え方 単一責任が明確 → 肯定評価が出やすい 既存依存で「責務分散」に見える → 冷静な評価が必要

最後の行が AI 時代の設計判断において重要な意味を持ちます (§ 4 で深掘りします)。

このような事例を抽象化すると、AI 時代でも踏襲すべき古典的設計原則が 3 つ浮かび上がります。冒頭ユーザー指摘の「人間時代でもそうでした」という言い方が示唆的でした。これらは AI 時代に新しく生まれた原則ではなく、人間時代から不変の原則です。

原則 内容
既存実装を活かす 要件 X を実現する際、既存実装に 追加 で対応する前に、既存実装から 減算 で実現できないかを必ず検討する。既存実装にはチームの暗黙知 + 環境との整合性 + テストされた品質が織り込まれている
コード変更量最小化 要件が満たせるなら変更行数は少ないほど良い。保守性 / バグ混入 risk / レビュー cost / ロールバック容易性 / 認知負荷、すべての軸に効く
Source of Truth 単一化 真実 (実装の存在 / 設定値 / マスターデータ) は 1 箇所で管理する。複数箇所で重複定義すると必ず divergence する

姉妹記事の 「実装工数は tie-breaker ではなくなった」 と本原則は矛盾しません。あちらは「堅牢性 vs 工数」軸の話で、本記事は「同じ堅牢性なら、コード変更量が少ない方を選ぶ」という、堅牢性が確保されたあとの第二軸の話です。両方を組み合わせると AI 時代の設計判断の優先順位はこうなります。

  1. 第一軸: 堅牢性 (ドメインモデルの正しさ・長期運用コスト・認知負荷)
  2. 第二軸: 同じ堅牢性なら、コード変更量が少ない方を選ぶ
  3. 第三軸: 同じ堅牢性かつ同じコード変更量なら、実装工数が少ない方を選ぶ (これは tie-breaker)

§ 3 蒸留: AI 時代でも踏襲すべき古典的設計原則は、(1) 既存実装を活かす、(2) コード変更量最小化、(3) Source of Truth 単一化 の 3 つ。AI が「綺麗そうな新規実装」を量産しがちな時代だからこそ、より意識的に踏襲する必要がある。


4. AI 時代特有の罠 — 「綺麗そうな新規実装」を量産しがち

ここからが、AI 時代特有の論点です。

なぜ今回、サブセッションは「許可リスト」approach を選んだのか。後付けで分析すると、AI には以下のような構造的バイアスがあるように見えます。

4-1. AI は「既存読まずに新規生成」の誘惑に弱い

AI に「特定ロール用のサイドメニューを実装して」と指示した場合、AI が取り得る approach は大きく 2 通りあります。

  • (a) 既存メニュー実装 (view / Controller / 関連 partial 等) を熟読し、そこから「不要な項目」を引き算するパッチを書く
  • (b) 新しい構造 (Constants クラス + middleware + view 修正) を起こして、要件にある N 項目を組み立てる

(a) の方が SoT を単一化できる優れた approach ですが、AI にとって認知的なコストが高い のです。理由は以下の通りです。

  • (a) は既存実装を全面的に読み込む必要がある (メニューロジック、描画構造、関連 Controller など)
  • (a) は既存実装の前提に依存するパッチになるため、汎用性が低い (= 学習データの中で「典型的な解法」として現れにくい)
  • (b) は学習データに溢れる「綺麗な責務分離パターン」(Constants + middleware + ServiceProvider) のテンプレートに乗りやすい
  • (b) はコードレビューでも肯定評価を得やすい (前述 § 3 参照)

つまり AI は、「既存資産を読むコストを払って減算する」より「学習データのテンプレートに沿って新規定義する」方を選びがち という構造的バイアスを持ちます。これは Claude や他の LLM の本質的な特性で、明示的に文脈を与えない限り、デフォルトでこの誘惑に流れます。

4-2. kickoff Phase 0-6 の Alternative Design に「既存活用 vs 新規定義」軸を必ず含める

task-kickoff プロトコル では、設計タスクの着手前に AI 自身に Alternative Design を 3 案以上列挙させる Phase 0-6 を設けています。今回の事例で振り返ると、AI が出した Alternative Design は「許可リスト approach」のバリエーション (Constants の置き場所、middleware の名前、ServiceProvider 登録の有無) に偏っており、「拒否リスト approach」が選択肢として視野に入っていなかった ことが分かります。

これは Phase 0-6 の運用に明示的なチェック項目を追加することで、構造的に防止できます。

Phase 0-6 強制チェック (拡張案):

  • 既存資産活用 approach (既存実装 + 引き算) を必ず Alternative Design の 1 つとして列挙する
  • 新規定義 approach (Constants / 新規 middleware / 新規 class) を選んだ場合、なぜ既存資産活用では実現できないか を明示する
  • 両者のコード変更量を概算で比較する

このチェックを入れていれば、「許可リスト approach」を選ぶ前に「拒否リスト approach も検討した結果、こちらが優位」という意思決定の透明性が確保されます。実際には拒否リスト approach の方が優位だと気付くはずなので、最初からそちらを選べた可能性が高い。

4-3. テスト設計の DoD に「環境 verify」を必須化する

もう一つの構造的対策は、テスト設計担当のサブセッションの DoD (Definition of Done) に 「コードベース + 既存環境での挙動 verify」を必須化 することです。

今回の事例では、許可リストの各項目について「該当 route が実在するか」を verify するステップが、テスト設計の DoD に明示的に含まれていませんでした。Unit テストでは、Constants の中身と middleware のロジックを検証することはできても、「Constants の中身が既存 route と整合しているか」までは catch できません。

AI 時代の利点は、この verify が並列 grep + 環境 curl で安価に実施できる ことです。

# 例: 許可リストの各 path が dev 環境で実在するか並列 verify
for path in "${ALLOW_PATHS[@]}"; do
    curl -sL -o /dev/null -w "%{http_code} $path\n" \
         "https://manage.dev02.example.com${path}"
done

人間がやれば手間ですが、AI なら 30 秒で終わります。AI 時代のテスト設計 DoD は、人間時代より厚くできる。むしろ厚くすべきです。

§ 4 蒸留: AI には「既存読まずに新規生成」の構造的バイアスがある。kickoff Phase 0-6 の Alternative Design に「既存活用 vs 新規定義」軸を必ず含めること、テスト設計 DoD に「環境 verify」を必須化することで、構造的に対策できる。AI による検証コストの安さは、対策の実装可能性を高める方向に効く。


5. アンラーニングと踏襲の見極め

冒頭のユーザー指摘の最後の部分を再掲します。

アンラーニングすべきことと、踏襲すべきことの見極めが重要ですね。

AI ネイティブ化の文脈で語られる「アンラーニング」は、しばしば 「古い前提を全部捨てよ」 というメッセージとして受け取られがちです。しかしユーザーの指摘は逆向きで、「アンラーニングすべきものと、踏襲すべきものを見極めよ」 という、より精緻な姿勢を要求しています。

整理するとこうなります。

5-1. AI 時代でアンラーニングすべきもの

アンラーニング対象 旧前提 新前提
実装工数の希少性 工数は希少資源 → 工数最小化が tie-breaker AI で工数は圧縮される → 堅牢性で決める (詳細)
計測コストの高さ 計測は半日仕事 → 推測で即決 計測は 10 秒 → まず計測 (詳細)
並列度を上げれば速い 人間工数稀少 → 並列度最大化 デバッグ範囲最小化が新パラダイム → 並列度抑制 (詳細)
AI に新規実装を任せれば cost ゼロ 工数 cost が低い → 量を出して OK AI ほど「綺麗そうな新規実装」の罠に陥りやすい → 既存活用を意識的に選ぶ (本記事)

5-2. AI 時代でも踏襲すべきもの

踏襲対象 原則 不変な理由
既存実装を活かす 追加より減算を先に検討 既存資産にはチームの暗黙知が織り込まれている
コード変更量最小化 要件が満たせるなら変更行数は少ない方が美しい 保守性 / バグ risk / レビュー cost / 認知負荷 全てに効く
Source of Truth 単一化 真実は 1 箇所で管理 重複は必ず divergence する
ドメインモデルの正しさ 実装工数より堅牢性 AI で工数 cost が下がっても堅牢性の便益は変わらない
認知負荷を意識する 読むコストを見積もりに含める コードは書くのは 1 回、読むのは N 回

両者を区別する判断軸はこうです。

  • アンラーニング対象: AI によってコスト構造そのものが変わったもの (工数、計測、並列度、新規実装)
  • 踏襲対象: AI によってコスト構造が変わらない、あるいは変わってもなお価値が変わらないもの (堅牢性、SoT 単一化、認知負荷)

「AI が安くしたものに依存した経験則を、順番にアンラーニングしていく」というスタンスは正しい。同時に、「AI が安くしなかった (= 本質的価値を持つ) 古典的原則を、踏襲していく」スタンスも同じくらい重要です。両者をバランス良く運用することが、AI ネイティブ時代のエンジニアの新しい技能になりつつあると感じています。

§ 5 蒸留: アンラーニングと踏襲は対立する概念ではなく、AI が変えた前提と変えていない前提を見極めるという、同じ姿勢の表裏。アンラーニングだけを推進すると、踏襲すべき古典的原則まで捨ててしまう。


6. 明日からの行動変容

抽象論で終わらせないために、具体的な行動に落とします。

6-1. 設計レビューに「既存活用 vs 新規定義」軸を加える

設計レビューで AI が提示した実装案を見たら、以下を問います。

  • 「この実装は新規定義か、既存資産からの引き算か?」
  • 「新規定義の場合、なぜ既存資産からの引き算では実現できないのか? その判断は明示的に行われたか?」
  • 「Source of Truth は何か? 二重化していないか?」

AI のコードレビューが「単一責任が明確で良い」と返してきても、それだけでは不十分です。「既存資産との整合」軸での評価 を、人間レビューアが意識的に重ねる必要があります。

6-2. kickoff Phase 0-6 の Alternative Design テンプレートを更新する

設計タスク着手時の kickoff (詳細は task-kickoff プロトコル) で、Alternative Design の列挙テンプレートに以下を追加します。

Alternative Design 必須軸:
- 案 X: 既存資産活用 approach (既存実装からの引き算で実現)
- 案 Y: 新規定義 approach (Constants / middleware / class 等を新規追加)
- 案 Z: ハイブリッド approach
評価軸:
- コード変更量の概算
- Source of Truth の単一性
- 既存資産との整合

このテンプレートを skill / プロトコルレベルで永続化することで、AI が「既存読まずに新規生成」のデフォルトに流れる構造的バイアスを、運用レベルで catch できます。

6-3. テスト設計 DoD に「環境 verify」を必須化する

テスト設計担当のサブセッションの DoD に、以下を必須項目として追加します。

  • コードベース全体での grep verify (関連シンボルの存在 / 使用箇所)
  • 既存環境 (dev / dev02 等) での挙動 verify (curl / API call / DB query)
  • 上記が完了するまで、テストケースの設計は完了とみなさない

AI 時代の利点は、これらの verify が安価で並列に実施できることです。「人間時代より厚いテスト DoD」を設計するチャンスとして使うべきです。

6-4. 「コード変更量」を見積もりに含める

実装工数の見積もりに、「変更行数」 を必ず含めます。同じ要件を満たす A 案と B 案で、変更行数が大きく違うなら、それは意思決定の重要なインプットです (堅牢性が同じ前提で)。

同時に、「将来の変更量」 も見積もります。新規メニュー追加時、path 改名時、要件変更時に、A 案と B 案でそれぞれ何行の変更が必要かを想像する。今回の事例だと、許可リスト approach は新規メニュー追加で複数箇所同期更新、拒否リスト approach は既存に追加するだけ、の差です。


まとめ

AI 時代に新しく学ぶことの量は確かに多いですが、それと同じくらい、人間時代から 踏襲すべき古典的設計原則 があります。

要件が満たせるならコード変更は少ないほど美しい。これは人間時代でもそうでしたが、AI 時代でも変わりません。

AI が「綺麗そうな新規実装」を量産しがちな時代だからこそ、既存実装を活かす選択を意識的に取る必要があります。許可リスト vs 拒否リストの選択は、その本質を象徴する具体例でした。

観点 許可リスト 拒否リスト
Source of Truth 二重 (新規 + 既存) 単一 (既存)
コード変更量 多い (新規 class / middleware / 登録の連鎖) 少ない (view 側条件分岐 + 最小 middleware)
AI が選びがち ✅ (テンプレート的に綺麗) (既存読み込みが必要で認知 cost 高)
環境 verify なし時の事故 risk 高い (divergence 顕在化) 構造的に低い
長期保守性 divergence 潜在 divergence 構造的に発生不能

AI 時代のエンジニアは、AI が出してきた「綺麗そうな新規実装」を、「もっと変更量を減らせないか」 という古典的な問いで受け止め直す技能を持つ必要があります。アンラーニングすべきことと踏襲すべきことの見極めが、その技能の核心です。

次にあなたが AI から実装案を受け取ったら、こう問いかけてみてください。

もっとスマートに実現できませんか?

その一言で、AI はコード量を減らし、副作用を抑え、保守性の高い実装を考え直してくれます。あなたの役割は、そのスイッチを押すことです。


関連記事

GitHubで編集を提案

Discussion