TL;DR
AHAは「Avoid Hasty Abstractions(性急な抽象化を避けよ)」の略である。Kent C. Doddsが紹介・普及した概念で、元は Cher Scarlett による。
AHA Programming 💡kentcdodds.com
核心は Sandi Metz の言葉に集約される。
「間違った抽象化よりも、重複を選べ」
これは「抽象化するな」ではなく「確信が持てるまで待て」という意味である。
3つの指針
- 重複を恐れない:最初から抽象化しない。重複はそのままにする。
- パターンを観察する:重複が増えたとき、共通点と相違点の境界線を見極める。
- 確信が持てたら抽象化する:境界の明確化により抽象化を行う。
DRY・WET・AHAの比較
問題点
なぜ早すぎる抽象化は問題なのか。
要点
- 抽象化は将来予測であり、外れやすい
- 外れた抽象化は条件分岐で肥大化する
- 重複のコストは線形だが、誤抽象化は指数的に増える
抽象化は正しく見えても、未来の要件変化でズレることがある。そのズレが積み上がると、理解と修正が困難なコードになる。
重複 vs 間違った抽象化のコスト比較
実例:過度に抽象化された商品カード
過度な抽象化が引き起こす問題を、具体的なコード例で示す。
状況
例えば、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(新規追加)
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>
);
各ファイルは独立。共通部分は重複しているが、自己完結している。
「限定商品」を追加する場合
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つ(新規作成)。他コンポーネントへの影響なし。
比較
対応方法
間違った抽象化から回復するための手順を示す。
ステップ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との協働
- LLMの提案を疑う:「この重複を共通化しました」に対して「本当に共通化すべきか?」を考える
- 意図を明示する:「この重複はあえて残している」というコメントをコードに残す
- 段階的に依頼する:「まず重複したまま書いて」→「パターンを観察して」→「必要なら抽象化して」
参考