TL;DR
- 良い抽象化とは「変更理由が同じものをまとめ、違うものを分ける」こと。コードの行数削減そのものは目的ではありません。
- 過剰抽象は早すぎる一般化(YAGNI違反)で起き、不足抽象は重複と密結合で起きます。痛みの種類が逆なので対処も逆になります。
- 「いつ抽象化するか」は Rule of Three(3回目の重複で初めて抽象化)が実務的な目安です[経験則]。
- AI生成コードは「それらしい共通化」を即座に提案するため、早すぎる抽象化が混入しやすい点に注意します。
- 迷ったら「重複を許す方が、間違った抽象に縛られるより安い」(Sandi Metz)を判断の支点にします。
はじめに
「この処理、共通化しておきましょうか?」——レビューで頻出するこの一言は、良い設計にも悪い設計にも転びます。抽象化は強力ですが、やりすぎると過剰抽象、やらなさすぎると不足抽象になり、どちらもコードの寿命を縮めます。
この記事は、良い抽象化の定義、過剰・不足それぞれの症状、「いつ抽象化すべきか」の判断軸を、コード比較を交えて整理する解説記事です。対象読者は、設計判断を任され始めた中堅エンジニアと、AI生成コードのレビュー担当者です。
結論:良い抽象化は「変更理由」でまとめる
先に結論を述べます。良い抽象化の基準は、行数でもDRY原則の機械的適用でもありません。「変更理由が同じコードをまとめ、違うコードを分ける」——これが軸です。
これは Robert C. Martin の単一責任の原則(SRP)が言う「クラスを変更する理由は1つであるべき」という考えと一致します(Clean Coder Blog: The Single Responsibility Principle)。見た目が似ているだけのコードを共通化すると、片方の変更が他方を壊し、結局「共通化を剥がす」作業が発生します。
Martin Fowler はリファクタリングを「外部から見た振る舞いを変えずに内部構造を改善すること」と定義しています(martinfowler.com: Refactoring)。良い抽象化は、この「振る舞いを変えない安全な構造改善」の積み重ねとして、重複が現れてから事後的に現れるのが理想です。
過剰抽象:早すぎる一般化の罠
過剰抽象は、まだ存在しない要求を見越して汎用的な仕組みを先に作ったときに発生します。典型例は次の通りです。
- 1か所でしか使わない処理に、設定可能なオプションを5つ持たせる
- 将来の拡張に備えた抽象基底クラスやインターフェースを、実装1つしかないのに切る
- ストラテジーパターンやファクトリを「念のため」で導入する
これは YAGNI(You Aren't Gonna Need It)の違反です。c2 wiki では YAGNI を「実際に必要になるまで機能を追加するな」と説明しています(c2.com: You Arent Gonna Need It)。早すぎる一般化が厄介なのは、間違った抽象が後続のコードを巻き込んで固定化する点にあります。
Sandi Metz は講演「All the Little Things」で、有名な原則を残しています——「duplication is far cheaper than the wrong abstraction(重複は、間違った抽象よりはるかに安い)」(sandimetz.com: The Wrong Abstraction)。誤った抽象は、剥がすのに重複以上のコストがかかるため、確証がないなら重複を残す方が合理的だという主張です。
次のTypeScriptは過剰抽象の例です。通知が1種類しかない段階で、汎用の通知エンジンを組んでしまっています。
// 過剰抽象:実装はメール1種類なのに、汎用化を先取りしている
interface NotificationChannel {
send(payload: NotificationPayload): Promise<void>;
}
interface NotificationPayload {
recipient: string;
subject?: string;
body: string;
metadata?: Record<string, unknown>; // 使う予定のない拡張ポイント
}
class NotificationDispatcher {
constructor(private channels: Map<string, NotificationChannel>) {}
async dispatch(channelKey: string, payload: NotificationPayload) {
const channel = this.channels.get(channelKey);
if (!channel) throw new Error(`unknown channel: ${channelKey}`);
await channel.send(payload);
}
}
// 呼び出し側もキー解決の手間を負う
await dispatcher.dispatch("email", { recipient: user.email, body: "ようこそ" });
実態は「ユーザーにウェルカムメールを送る」だけです。この段階では、抽象は読み手の認知負荷を増やすだけで、何の柔軟性も生んでいません。適切な抽象は次のように、やりたいことをそのまま書く形です。
// 適切:要求は1つ。素直な関数で十分
async function sendWelcomeEmail(user: User): Promise<void> {
await mailer.send({
to: user.email,
subject: "ようこそ",
body: renderWelcomeBody(user),
});
}
SMS やプッシュ通知という2つ目・3つ目の実装が現れてから、共通の NotificationChannel を抽出すればよいのです。その時点なら、抽象は実在する差分に裏打ちされ、外しにくい設計になります。
不足抽象:重複と密結合の蓄積
逆の失敗が不足抽象です。これは「同じ変更理由を持つコード」が複数箇所にコピーされ、片方を直し忘れる事故として表面化します。
- 同じバリデーションロジックが3つのコントローラに散在する
- 料金計算式が画面・バッチ・APIにそれぞれ書かれ、改定時に1つだけ漏れる
- 内部データ構造を直接参照することで、モジュール間が密結合になる
Martin Fowler は、重複は最もよく現れるコードの不吉な臭い(Code Smell)の一つだとしています(refactoring.com: Smells)。不足抽象の怖さは、変更が「漏れ」として顕在化する点です。テストが薄い箇所では、本番障害になって初めて重複に気づきます。
refactoring.guru は、重複コードに対する基本対処として「同じ処理を1か所にまとめ、Extract Method / Extract Class で抽出する」ことを挙げています(refactoring.guru: Duplicate Code)。不足抽象の解消は、過剰抽象と違って根拠が明確です。すでに重複が観測されているので、抽出して困ることはまずありません。
判断軸:いつ抽象化すべきか
過剰と不足の境界を分けるのが「タイミング」です。実務で使える軸を3つ示します。
1. Rule of Three(3回目で抽象化する)
最も知られた目安が Rule of Three です。Martin Fowler は著書『Refactoring』で、Don Roberts の経験則として「1回目はそのまま書く。2回目は重複に気づいても(渋々)コピーする。3回目に現れたら抽象化する」を紹介しています[経験則](martinfowler.com: Refactoring)。
なぜ2回ではなく3回かというと、2回の一致は「偶然似ているだけ」の可能性が残るからです。3回目まで待つと、それが本当に共通の変更理由を持つパターンかを見極める証拠が揃います[経験則]。
2. 変更理由が一致するか
回数だけでは不十分な場合があります。判断の本質は「これらは同じ理由で同時に変わるか」です。同時に変わるなら抽象化、別々に変わるなら分離します。料金計算のように仕様が一本化されているものは1回目でも抽象化してよく、逆に偶然形が似ているだけのものは10回重複しても共通化すべきではありません。
3. 抽象の「逃げ道」があるか
抽象化を入れるときは、間違えたら剥がせるかを同時に考えます。Sandi Metz の指摘の通り、誤った抽象を後から剥がすのは高コストです。インターフェースの利用箇所を局所に閉じ込め、依存方向を一方向に保つと、撤退が容易になります。この依存方向の設計は、責務境界の話と地続きです。詳しくは 責務分離が崩壊する理由と設計原則 を参照してください。
AI生成コードでの抽象化の罠
AI生成コード(LLMによる補完・エージェント)は、抽象化の判断を一段難しくします。LLMは学習データに「きれいに共通化されたコード」を大量に含むため、1回目の実装でも汎用的な抽象を提案しがちだからです。結果として、要求が1つしかない段階で過剰抽象が混入します。
逆方向の罠もあります。エージェントが「最短で動くコード」を優先するモードでは、既存の共通関数を見つけられず重複を再生産することがあります。つまりAIは過剰抽象と不足抽象の両方を同時に持ち込みうるのです。
対策はシンプルです。AIの提案を「まだ存在しない要求に備えていないか」「既存の抽象を見落として重複させていないか」の2点でレビューします。AI生成コードがもたらす設計上の技術的負債の蓄積メカニズムは、AI生成コードと技術的負債 で扱っています。判断基準を言語化してレビューに組み込むことが、過剰・不足どちらの暴走も止める唯一の歯止めです。
まとめ
良い抽象化は「変更理由でまとめる」ことであり、行数削減やDRYの機械的適用ではありません。過剰抽象は早すぎる一般化(YAGNI違反)で、不足抽象は重複と密結合で起きます。実務では Rule of Three を出発点にしつつ、「同じ理由で同時に変わるか」「間違えたら剥がせるか」を併せて判断してください[経験則]。
次のアクションとして、直近のレビューで「共通化しますか?」が出たら、回数・変更理由・撤退容易性の3点を声に出して確認する習慣から始めてみてください。
FAQ
Q. Rule of Three は必ず守るべきルールですか? A. 絶対のルールではなく経験則です[経験則]。料金計算のように仕様が一本化された処理は1回目でも抽象化してよく、逆に偶然似ているだけのコードは複数回重複しても共通化すべきではありません。回数より「同じ変更理由を持つか」を優先します。
Q. 過剰抽象と不足抽象、どちらがより危険ですか? A. 一般には過剰抽象の方が剥がしにくく高コストです。Sandi Metz が言うように「重複は間違った抽象よりはるかに安い」ため、確証がなければ重複を残す判断が合理的です[経験則]。不足抽象は根拠が明確なので、観測してから抽出すれば安全に解消できます。
Q. AI生成コードの抽象化はどうレビューすればよいですか? A. 「まだ存在しない要求に備えた過剰抽象になっていないか」「既存の共通処理を見落として重複していないか」の2点を確認します。AIは両方の失敗を同時に持ち込みうるため、変更理由ベースの判断を人間がレビューで補う必要があります。
Q. DRY原則を守れば良い抽象化になりますか? A. なりません。DRYは「知識の重複を避ける」原則であり、見た目が似たコードを機械的にまとめることではありません。変更理由が異なるコードをDRYのために共通化すると、かえって密結合な過剰抽象を生みます。
