Tsuzura

コードの抽象化とは何か

AIが共通化やinterfaceを増やす前に、隠す実装詳細と利用側へ残す契約を分け、抽象化の採否を一件ずつ判断する方法を整理します。

AIが「重複をまとめて抽象化しました」と報告しても、その変更が適切とは限りません。関数やファイルが分かれ、異なる中身を同じ呼び方で使うためのinterfaceという言語上の宣言が増えただけかもしれません。確認するのは形ではなく、何を隠し、利用するコードに何を約束として残したかです。

ここでいうコードの抽象化は、「重要な違いを残しながら、利用側に不要な詳細を隠し、名前・入力・出力・失敗などの契約から使えるようにする設計判断」です。利用側が必要とする情報まで消すことや、似て見える処理を一つにすることではありません。

完成状態は、一件の候補について、利用箇所、異なる点、変更理由、隠す詳細、残す契約、コスト、未確認事項を判断票へ書き、次のどれかを人が選べた状態です。

  • 現状を維持する
  • 小さく切り出す
  • 契約を作る
  • 保留する
  • 元へ戻す

すぐに確認を始める場合は、一つの抽象化判断票へ進んでください。AIには先に読み取り専用(read-only)の調査だけを依頼します。大量のリファクタリング(動作を変えずにコードを整理する変更)、interfaceの自動生成、権限・トランザクション(複数の処理をひとまとまりで成功または失敗にする単位)・エラー時の約束の変更は、この段階では実行させません。

コードの抽象化とは何か

抽象化では、実装を使うために必要な約束と、利用側が知らなくてもよい詳細を分けます。約束には、少なくとも名前、受け取る入力、返す出力、失敗時の扱いがあります。状態を変える処理では、保存や送信などの副作用も契約に含めます。誰が何を行えるかを確かめる認可、トランザクション、性能条件が結果を左右するなら、それらも契約から落とせません。

David Parnasの原論文は、コードのまとまり(モジュール)の使い方を示す仕様について、ほかのソフトウェアが追加情報なしで対象モジュールと相互作用できるだけの情報を示し、それ以上の不要な情報は仕様へ含めないという二つの目標を置いています(McMaster Universityの原論文記録)。本記事は、この「利用に十分で、実装を必要以上に見せない」という範囲を実務上の定義に使います。

LiskovとZillesは、データの中身を直接扱わず、操作を通じて使う「データ抽象化」の仕様を、概念とそれを実現する複数のプログラムの間に置きました。また、操作名や型で表現する仕様と、表現の詳細を持ち込む仕様を区別しています(Specification Techniques for Data Abstractions)。ここから「実装を一つに固定せず、利用側が観測する操作を残す」という範囲を使います。

Lampsonはコンピューターシステムのインターフェースを、実装と利用側を分け、互いに必要な前提を示すものとして説明しました(Hints for Computer System Design)。同じ論文は、インターフェースを必要最小限に保ち、理解していない将来要件へ汎用化し過ぎないこと、機能だけでなく利用コストも考えることを勧めています。

三つの資料は対象がそれぞれモジュール仕様、データ抽象化、システムインターフェースであり、抽象化の唯一の標準定義ではありません。ここでは共通する部分を、一件のコード変更に使える形へ整理しています。すべての抽象化に、数式や厳密な記法で表す形式仕様を求めたり、経験則をあらゆる関数へ当てはめたりはしません。

良い抽象化で利用側から見えるもの

利用側は、内部の手順ではなく次の情報から使える状態になります。

  • 何をするものか分かる名前
  • 許される入力と、満たす必要がある条件
  • 返る値、状態変化、外部への副作用
  • 起こり得る失敗と、そのとき利用側が取る行動
  • 権限、トランザクション、時間や資源の条件
  • 契約を満たす限り差し替えられる実装

隠せるのは、これらの約束を変えずに交換できる詳細です。利用側が特定のDBテーブル名、SDK(外部サービスを操作する開発用部品)のメソッド名、内部の並び順を知らなくても使えるなら、隠す候補になります。反対に、認可の結果や、DBへの変更を確定するコミットの単位によって利用側の行動が変わるなら、それは単なる内部詳細ではありません。

先に六つの言葉をそろえる

言葉 この記事での意味 確認する問い
情報隠蔽 利用側が判断に使わない実装詳細を、契約の外へ置くこと その詳細を変えても利用側のコードと期待結果は変わらないか
契約 利用側と実装側が依存してよい約束 名前、入力、出力、失敗、状態、副作用、条件は何か
実装 契約を満たす具体的な手順や技術 交換してよい手順と、固定すべき挙動を分けられるか
利用側 抽象を呼び出し、結果や失敗を受け取るコード どこから、何を期待して使っているか
依存 一方の変更が、もう一方の変更や確認を必要にする関係 何を変えると、どの利用箇所へ影響するか
境界 利用側と実装側の責任を分ける位置 入力を受け、結果・失敗・状態を返す位置はどこか

契約は成功時の返り値だけではありません。MeyerのDesign by Contract(契約による設計)は、呼び出し側(client)と実装側(supplier)の責任を、事前条件(precondition)と事後条件(postcondition)で分けます(Applying Design by Contract)。同じ論文では、異常値を呼び出し側が避ける条件にするか実装側が扱うかを明示し、契約を満たせない状態を失敗として説明しています。本記事ではEiffelの構文を要求せず、入力条件、完了後の約束、失敗の責任を残す考え方だけを使います。

関数分割やinterface追加とは同じ意味ではない

変更の形 その変更だけで分かること まだ分からないこと
関数へ分ける 処理の一部に名前と呼び出し位置ができた その名前が安定した意味を持つか、利用側へ契約を作るべきか
共通化する 複数箇所が同じコードを通る 同じ業務判断か、同じ理由で変わるか、差を失っていないか
interfaceを追加する 言語やフレームワークの仕組みで型の境界が増えた 何を隠すか、契約が十分か、実装詳細を漏らしていないか
汎用化する 入力や設定で扱える場合が増えた 現在必要な場合か、名前と契約を簡潔に保てるか
ファイルを分ける 保存場所と読み取り単位が変わった 依存方向や変更理由が分かれたか

Fowlerのリファクタリング一覧には、処理を関数へ出すExtract Functionと、呼び出しを中へ戻すInline Functionの両方があります(Catalog of Refactorings)。関数分割は有効な整理ですが、分けた事実だけでは、その関数を長く維持する抽象にするか決まりません。

interfaceも同じです。Java SE 25では、インターフェースのメソッドにabstractだけでなくdefault、private、staticがあり、メソッド本体を持つ場合があります(Java Language Specification §9.4)。これはJava固有の言語仕様です。interfaceというキーワードの有無と、利用側に適切な契約を示せているかは別の確認です。

Goの公式コードレビュー指針は、インターフェースを利用側へ置き、モック(mock)のためだけに実装側へ定義せず、現実の利用例がないうちに先回りして作らないよう勧めています(Go Code Review Comments: Interfaces)。これはGo固有の指針です。ほかの言語にもそのまま当てはめませんが、「利用条件が見える前に型だけ作らない」という保留理由には使えます。

一つの抽象化判断票

一度に一件だけ記録します。欄を埋められない場合は推測せず、「未確認」、確認する人、再開条件を書きます。この票は公式規格の様式ではなく、この記事で判断の観点をそろえるための空欄テンプレートです。

欄 記入する内容 記入欄
対象 関数、クラス、モジュール、外部サービス、DB処理等の識別子
利用側 呼び出し元、定期処理、画面、API、テスト
同じ業務判断 複数箇所で本当に同じ意味の判断
異なる点 入力、例外、権限、順序、性能、変更理由の差
変更理由 誰が、何の変更で、このコードを直すか
隠す詳細 利用側が知らずに交換できる手順・技術
名前 実装手段ではなく、利用側から見た役割
入力と事前条件 許す値、欠落、順序、前提状態
出力と完了条件 値、状態、保存結果、通知
失敗 分類、返し方、再試行の可否、部分完了の有無
状態・副作用 永続化、送信、キャッシュ、ログ等
認可 誰が、どのリソースへ、何を許可するか
トランザクション 開始、DBへの変更を確定するコミット、途中の変更を取り消すロールバックを持つ位置
性能条件 遅延、回数、資源量で守る条件
置換実装 現在または確認済みの別実装
テスト 契約、実装、統合をどこで確認するか
影響範囲 契約変更時に確認する利用箇所
担当者 採否と契約変更を承認する人
未確認・再開条件 誰が何を確認したら判断を再開するか
判断 維持/小さく切り出す/契約を作る/保留/元へ戻す
戻す条件 どの兆候で抽象を外すか
戻す方法 戻す対象、手順、復旧を確認するテスト

記入は、利用側と差異、変更理由、契約、コストの順に進めます。実装の見た目から名前を先に決めると、違う処理を同じ言葉で包みやすくなります。

抽象化を使う候補

次の条件は調査を始める合図です。一つ当てはまっただけで採用は決まりません。利用側と異なる点を列挙し、契約を短く説明できるかまで確認します。

候補になる状況 確認すること 採用を止める反証
同じ業務判断が複数箇所にある 同じ入力なら同じ結果・失敗になるか。同じ人の変更で直るか 見た目だけ同じで、部門・規則・変更時期が違う
複数の実装を同じ利用方法で扱う 実装が違っても守れる最小の契約は何か 一つの実装にしかない引数や状態を契約へ露出する
外部サービスやDBとの境界がある SDK、通信、問い合わせ等を変えても残す業務上の約束は何か 外部サービスのエラーやDBトランザクションを消してしまう
変更しやすい詳細へ複数箇所が依存する 変更頻度、変更理由、影響箇所に実績があるか 将来変わりそうという予想だけで利用例がない
テストで実物の依存先を置き換える必要がある どのテストで、何を制御し、どの契約を確認するか テスト専用のメソッドを本番契約へ混ぜる

FowlerはGerard Meszarosの用語を引き、Test Double(テストダブル)を、テスト中に実物のオブジェクトの代わりとして使うものと説明しています(Mocks Aren’t Stubs)。外部送信や時刻、課金サービス等をテストで制御する必要は境界を作る候補になります。本記事では、置換したいという理由だけなら、本番コードのインターフェース作成を保留します。実際の利用側が必要とする最小契約を先に確認します。

複数実装は、契約の候補を見つけやすくします。けれども、単一実装でも、外部サービス変更やDB接続詳細を利用側へ広げない価値がある場合はあります。反対に、複数実装があっても重要な違いを一つの契約で表せないなら、別々に保ちます。

使わない・保留する条件

条件 保留する理由 次に確認すること
一度しかない具体的な処理 境界を増やす利益より、追跡する場所が増える 次の利用箇所や変更実績が生じたか
要件が分からない 契約へ残す重要な違いを判断できない 業務責任者、受入条件、失敗条件
見かけだけの重複 コードの形が同じでも意味と変更理由が違う 誰が、なぜ、いつ変更するか
名前を付けられない 利用側から見た一つの役割がまだない 動詞、対象、結果を一文で言えるか
処理を通すだけの層(pass-through layer)になる 入力と出力をそのまま渡し、責任も情報隠蔽も増えない その層だけが持つ判断・変換・保護条件
変更理由が異なる処理をまとめる 一方の変更が無関係な利用側へ波及する 変更主体、規則、公開時期を分ける
契約が実装詳細を漏らす 置換時にもSDKの型、テーブル、内部順序を維持することになる 利用側が本当に観測する値・状態・失敗だけへ戻せるか

判断できない状態は失敗ではありません。「担当者」と「未確認・再開条件」を埋めて保留します。「いったん汎用的に作る」は保留の代わりになりません。

抽象化のコストとリスクを先に数える

抽象化はコードを短くする操作ではなく、依存の置き方を変える操作です。採用前に、得るものと同じ票でコストを確認します。

コスト・リスク 起こること 判断票で残すこと
間接参照 呼び出し元から実際の処理まで追う段階が増える 呼び出し経路と実装の発見方法
ファイル・型の増加 名前、配置、生成物、設定の管理が増える 増える構成物と担当者
デバッグ困難 失敗が境界で変換され、元の原因や状態を追いにくい エラーの連鎖、観測点、再現テスト
誤った共通化 重要な差を条件分岐やフラグで押し込む 差異と、分離へ戻す条件
例外の隠蔽 利用側が再試行、代替、停止を判断できない 失敗の分類と責任
トランザクション境界の隠蔽 部分完了、二重実行、コミット位置を誤認する 開始、コミット、ロールバック、DB外副作用
認可境界の隠蔽 誰が何を許可したか追えず、下位処理へ権限を広げる 判定点、強制点、リソース、操作
変更の波及 契約変更が全利用箇所の修正・再テストを必要にする 利用箇所一覧、互換性、移行方法

PostgreSQL 18のトランザクション説明では、複数の処理を「全部実行するか、何もしないか(all-or-nothing)」に束ね、中間状態をほかのトランザクションへ見せず、途中で完了できなければどの処理もDBへ反映しません(PostgreSQL 18: Transactions)。この出典が保証する範囲は、PostgreSQL内のトランザクションです。DB外のメッセージ送信や外部サービス呼び出しは、同じ完了単位かを別途確認します。抽象化の下へトランザクションを隠すなら、利用側がどの単位を一回の完了と見なすかを契約へ残します。

利用側へ公開した契約は、後から変えるコストも持ちます。.NETの公開ライブラリ互換性規則では、公開メンバーや引数の名前変更・削除は、それを使うコードを壊すため認めない変更として扱われます(.NET API compatibility rules)。これは.NETの公開APIに固有の規則ですが、利用側が依存する名前や入力を変えると影響が波及する具体例です。内部の名前まで早く公開契約にしない理由にもなります。

AIに頼むのは読み取り専用の調査まで

最初の依頼では、変更を許可しません。対象リポジトリで、次を列挙させます。

コードを変更せず、読み取り専用(read-only)で調査してください。抽象化の候補ごとに、対象、全利用箇所、共通する業務判断、異なる入力・出力・失敗・権限・トランザクション、関連テスト、変更履歴から分かる変更理由を列挙してください。分からない項目は推測せず未確認とし、確認するファイル・人・資料を示してください。リファクタリング案は、隠す詳細、残す契約、コスト、元へ戻す条件まで説明してください。

AIの出力は証拠一覧であり、採用決定ではありません。人は、利用箇所が漏れていないか、差異を小さく見せていないか、テストが契約ではなく現在の実装だけを固定していないかを確認します。

変更へ進むなら、少なくとも契約どおり成功する場合、契約どおり失敗する場合、利用側と実装を組み合わせた場合を確認するテストを選びます。現状維持や保留ならコードは変えず、既存テストがどの範囲を守っているかだけを記録します。

この段階でAIへ許可しない操作は次のとおりです。

  • 大量のファイルをまたぐリファクタリング
  • interface、基底クラス、ラッパーの一括生成
  • 認可の判定位置、許可・拒否条件の変更
  • トランザクションの開始、コミット、ロールバック位置の変更
  • エラー型、例外、再試行、部分完了の契約変更
  • 利用側の移行、削除、公開APIの名前変更
  • 最終採否と、未確認事項の代行判断

調査後も、変更は一件ずつ実施します。変更案、期待する動作、テスト、影響範囲、戻し方を固定してから書き込みを許可します。

五つの判断から一つを選ぶ

判断 選ぶ条件 次の行動
現状を維持する 具体的な処理が一度だけで、境界のコストが上回る 判断理由と見直し条件だけ残す
小さく切り出す 読みやすさやテスト準備のため分けたいが、公開契約はまだ不要 局所関数等の狭い範囲に保つ
契約を作る 複数利用・複数実装・外部境界があり、名前と失敗を含む最小契約を説明できる 利用側テスト、実装テスト、移行範囲を決める
保留する 要件、差異、担当者、権限、トランザクション、失敗のどれかが未確認 再開条件と確認担当を指定する
元へ戻す 処理を通すだけの層、フラグ増加、デバッグ悪化、変更波及が利益を上回る 呼び出しを関数内へ戻すか分離し、不要な構成物を外す

「小さく切り出す」と「契約を作る」は別です。前者は局所的な整理に留められます。後者は利用側が依存してよい約束を作るため、変更責任と互換性を引き受けます。

重要な設計判断として残す必要が生じたら、採否後にADRの書き方へ渡します。ADRは判断方法の代わりではなく、選んだ理由と見直し条件を維持する記録です。

抽象化を完了とするチェックリスト

  • 対象を一件に限定した
  • 全利用箇所を列挙した
  • 共通する業務判断と異なる点を分けた
  • 変更理由と変更する主体を確認した
  • 隠す詳細を、契約を変えず交換できるものに限定した
  • 名前が利用側から見た役割を表している
  • 入力、出力、失敗、状態、副作用を書いた
  • 必要な認可とトランザクションの境界を書いた
  • 性能条件と間接コストを確認した
  • 契約テスト、実装テスト、統合テストの役割を分けた
  • 契約変更の影響箇所を列挙した
  • 担当者、未確認事項、再開条件を残した
  • AIの書き込み権限と禁止変更を分けた
  • 五つの判断から一つを選んだ
  • 元へ戻す条件と方法を決めた

テストの種類と境界を分ける必要があれば、テストの種類と選び方で確認します。AIが変更を作った後は、AIが生成したコードのレビューで完成差分を確認します。システム全体のコード配置を変える判断はコードアーキテクチャの種類と選び方、SOLIDやDRY等の原則を選ぶ判断はソフトウェア設計の基本原則の範囲です。

最初は一件だけを読み取り専用で調べ、判断票を埋めます。契約条件が一つでも分からなければ、推測で抽象化せず、担当者と再開条件を決めて保留します。

出典

以下の資料は、いずれも2026-08-20に本文を直接確認しました。