零弐壱蜂

過剰な抽象化を避ける実装論としてのAHA Programming

TL;DR

AHAは「Avoid Hasty Abstractions(性急な抽象化を避けよ)」の略である。Kent C. Doddsが紹介・普及した概念で、元は Cher Scarlett による。

AHA Programming 💡kentcdodds.com

核心は Sandi Metz の言葉に集約される。

「間違った抽象化よりも、重複を選べ」

これは「抽象化するな」ではなく「確信が持てるまで待て」という意味である。

3つの指針

  1. 重複を恐れない:最初から抽象化しない。重複はそのままにする。
  2. パターンを観察する:重複が増えたとき、共通点と相違点の境界線を見極める。
  3. 確信が持てたら抽象化する:境界の明確化により抽象化を行う。

DRY・WET・AHAの比較

原則スタンス問題点
DRY知識の重複を避ける早すぎる抽象化を招く
WET2回まで重複を許容回数は本質的な基準ではない
AHA正しい形が見えるまで待つ

問題点

なぜ早すぎる抽象化は問題なのか。

要点

  • 抽象化は将来予測であり、外れやすい
  • 外れた抽象化は条件分岐で肥大化する
  • 重複のコストは線形だが、誤抽象化は指数的に増える

抽象化は正しく見えても、未来の要件変化でズレることがある。そのズレが積み上がると、理解と修正が困難なコードになる。

重複 vs 間違った抽象化のコスト比較

コスト種別重複コード間違った抽象化
増加のし方線形(3箇所なら3箇所修正)指数的(条件分岐が絡み合う)
影響範囲自明(修正したファイルのみ)不明(どこに影響するか分からない)
認知負荷低い(具体的で読める)高い(抽象を理解する必要がある)

実例:過度に抽象化された商品カード

過度な抽象化が引き起こす問題を、具体的なコード例で示す。

状況

例えば、ECサイトの商品カードを1つの汎用コンポーネントで扱う設計をしたとする。
状態は「通常」「セール」を想定している。

// 過度に抽象化されたストラテジー例(条件分岐が膨らみやすい)
type ProductType = 'normal' | 'sale';

type StrategyOptions = {
  highlight?: boolean; // セール強調
  disabled?: boolean; // 購入不可
};

export const buildProductStrategy = (type: ProductType, options: StrategyOptions) => {
  // 例: 条件の優先度が増えるほど読みづらくなる
  if (options.disabled) {
    return 'disabled';
  }

  if (type === 'sale' && options.highlight) {
    return 'sale-highlight';
  }

  return type; // 条件が増えるたびに複雑化する
};

Strategy パターン、Factory パターン、Container/Presenter パターンが適用されている。一見すると整理された設計に見える。

問題:「限定商品」の追加

新しい要件として「限定商品」を追加する。限定バッジと残数表示が必要である。

どこを修正すればよいか分からない

  • どの層に追加するかが不明確になる
  • 既存分岐に条件が積み上がる
  • 影響範囲の見積もりが難しい

AHA的アプローチ:重複を許容した設計

src/components/
├── NormalProductCard.tsx
├── SaleProductCard.tsx
├── PreorderProductCard.tsx
├── SoldoutProductCard.tsx
└── LimitedProductCard.tsx(新規追加)
// SaleProductCard.tsx: セール専用の責務に閉じる
export const SaleProductCard: React.FC<Props> = ({ product, onAddToCart }) => (
  <div className="product-card">
    <span className="product-card__badge">SALE</span>
    <h3>{product.name}</h3>
    <p>¥{product.salePrice}</p>

    <button type="button" onClick={onAddToCart}>
      カートに入れる
    </button>
  </div>
);

各ファイルは独立。共通部分は重複しているが、自己完結している。

「限定商品」を追加する場合

// LimitedProductCard.tsx: 限定商品を自己完結で表現する
export const LimitedProductCard: React.FC<Props> = ({ product, onAddToCart }) => (
  <div className="product-card">
    <span className="product-card__badge">LIMITED</span>
    <h3>{product.name}</h3>
    <p>残り {product.remainingStock} 点</p>
    <p>¥{product.price}</p>

    <button type="button" onClick={onAddToCart}>
      カートに入れる
    </button>
  </div>
);

修正ファイル:1つ(新規作成)。他コンポーネントへの影響なし。

比較

観点過度な抽象化重複許容
新機能追加8ファイル以上修正1ファイル新規作成
バグ修正特定困難該当ファイルを直す
コードリーディング6層以上を行き来1ファイルで完結
共通部分の修正1箇所(ただし影響不明)複数ヵ所(ただし影響自明)

対応方法

間違った抽象化から回復するための手順を示す。

ステップ1:インライン展開

抽象化されたコードを呼び出し元すべてにコピーして戻す。

ステップ2:不要部分の削除

各呼び出し元で使っていないパラメータや条件分岐を削除する。

ステップ3:観察

重複コードを眺めて、真に共通するパターンを見つける。前回とは異なる境界線が見えてくる可能性がある。

ステップ4:再抽象化(必要なら)

十分な情報に基づいて、本当に共通する部分だけを小さく抽象化する。

// 本当に共通な部分だけを小さく抽出する
const ProductTitle: React.FC<{ name: string }> = ({ name }) => <h3>{name}</h3>;

const AddToCartButton: React.FC<{ onClick: () => void }> = ({ onClick }) => (
  <button type="button" onClick={onClick}>
    カートに入れる
  </button>
);
// インライン展開前:条件分岐が増殖する汎用関数
function renderButton(type: 'normal' | 'sale', highlight?: boolean) {
  if (type === 'sale' && highlight) {
    return 'sale-highlight-button';
  }

  if (type === 'sale') {
    return 'sale-button';
  }

  return 'normal-button';
}

// インライン展開後:ケースごとに責務を分割
function renderSaleButton(highlight?: boolean) {
  return highlight ? 'sale-highlight-button' : 'sale-button';
}

小さな共通部品を使いつつ、各コンポーネントは独立を維持する。

余談:LLM時代における抽象化

LLM時代におけるAHAについても個人的な見解を述べる。

LLMは抽象化にバイアスがかかっている

理由1:学習データのバイアス

学習元(OSSや技術記事)には「DRYを守ったきれいなコード」が多いため、「重複を見たら抽象化する」パターンを強く学習している可能性が高い。

理由2:コンテキストの限界

LLMは与えられた情報で最適化するため、以下の点は考慮が難しい。

  • プロジェクトの歴史と過去の失敗
  • 将来の要件変更の方向性
  • 「この重複はあえて残している」という意図

理由3:「何もしない」を出力しにくい

LLMは入力に対して何かを出力しようとする。「この重複はあえて残すべき」という判断は出力しにくい。

LLMへの指示例

悪い指示例:

このコードをリファクタリングしてください。

良い指示例:

このコードをリファクタリングしてください。

ただし、以下の点に注意してください:
- UserFormとAdminFormは将来の変更方向が異なるため、
  あえて別コンポーネントにしています
- バリデーションロジックの重複は許容しています
- 早すぎる抽象化を避け、AHAの原則に従ってください

LLMとの協働

  1. LLMの提案を疑う:「この重複を共通化しました」に対して「本当に共通化すべきか?」を考える
  2. 意図を明示する:「この重複はあえて残している」というコメントをコードに残す
  3. 段階的に依頼する:「まず重複したまま書いて」→「パターンを観察して」→「必要なら抽象化して」

参考