コンテンツにスキップ

YAGNI・KISS・DRY と早すぎる抽象化・早すぎる最適化

一言で言うと

設計をやりすぎないための3原則です。

  • YAGNI (You Aren't Gonna Need It): 「今必要ない機能」は作るな。必要になってから作れ。
  • KISS (Keep It Simple, Stupid): 同じ問題を解けるなら単純な方を選べ。
  • DRY (Don't Repeat Yourself): 同じ知識を2か所に書くな。

そして、やりすぎの二大形態が早すぎる抽象化早すぎる最適化です。

解決したい問題

第1部でここまで学んだ道具(分離・抽象化・ポリモーフィズム)は、使うほど良いものではありません。将来への投資はすべて、当たらなければ純粋なコストです。この3原則は投資の掛け金を抑えるための原則です。

YAGNI

問題のある実装(将来に備えすぎ)

// C++20(問題例)— 現在の仕様: セーブはローカルファイルのみ
class ISaveBackend { /* ... */ };          // 「いつかクラウド対応するかも」
class SaveBackendFactory { /* ... */ };    // 「バックエンドを設定で切り替えるかも」
class SaveMigrationManager { /* ... */ };  // 「バージョン移行が必要になるかも」
// 実際に呼ばれるのは LocalFileBackend だけ

何が問題か

  • 使われない柔軟性にも、読解コスト・保守コスト・テストコストは毎日かかる
  • 将来の予想は大抵外れる。クラウド対応が来たとき、この抽象が要件に合う保証もない(合わなければ「抽象を剥がしてから作り直す」二重コスト)。

ただしYAGNIの例外

後から変えるのが極端に高くつく決定は先に考える価値があります: セーブデータのフォーマット(後方互換)、ネットワークプロトコル、公開APIなど。YAGNIは「安く後付けできるもの」に適用する原則です。

KISS

同じ仕様を満たす実装が2つあるなら、単純な方が正解です。単純=行数が少ない、ではなく理解に必要な前提知識と追跡の手数が少ないこと。

// 仕様: 3種類の敵の出現重みを引く
// 過剰: 汎用重み付き抽選テンプレート+設定ファイルローダー+キャッシュ層
// KISS: これで十分
int PickEnemyType(std::mt19937& rng) {
    // 重み Slime:5, Bat:3, Golem:2
    std::discrete_distribution<int> dist({5, 3, 2});
    return dist(rng);
}

DRY

正しいDRY: 「知識」の重複を消す

// 悪い: 「クリティカルは1.5倍」という知識が2か所にある
int PlayerDamage(int atk) { return static_cast<int>(atk * 1.5f); }  // プレイヤー用
int EnemyDamage(int atk)  { return static_cast<int>(atk * 1.5f); }  // 敵用
// → 仕様変更(1.5→2.0)で片方だけ直して不整合、が典型事故

// 良い: 知識を1か所に
constexpr float kCriticalMultiplier = 1.5f;

間違ったDRY: 「偶然似ているコード」の統合

プレイヤーの移動と敵の移動が今たまたま同じ式でも、変更理由が違う(プレイヤーは操作感の都合、敵はAIの都合で変わる)なら統合してはいけません。統合すると、片方の変更がもう片方を壊す結合が生まれます。DRYは「同じ知識か」で判定し、「同じ字面か」で判定しない。

早すぎる抽象化

重複が2回目に現れた時点で共通化するより、3回目まで待つ(Rule of Three と呼ばれる経験則。C++のRule of Threeとは別物)。2例では共通部分と偶然の一致を見分けられないためです。抽象を作るのに最適なタイミングは、具体例が出揃って共通部分が確信できたときです。

早すぎる最適化

  • 計測せずに「ここは遅いはず」で複雑化するのが早すぎる最適化。大抵は外れ、可読性だけ失います。
  • ただしアーキテクチャレベルの性能決定(データレイアウト、フレーム構造、アセットストリーミング)は後から変えられないので、最初に考えるのが正解です。「後で直せる細部の最適化を先送りする」のであって「性能を考えない」ではありません(→ 判断ガイド: 最適化すべきかData Locality)。

それぞれの緊張関係

  • DRYとYAGNIは衝突します: 共通化(DRY)は将来の変更に賭ける投資(YAGNI違反になりうる)。→ 「知識の重複は即DRY、字面の重複は3回待つ」が実務的な折り合い。
  • KISSとOCPも衝突します: 拡張に開くほど単純さは失われる。→ 拡張が実際に来る証拠(既に2回追加された等)を待つ。

ゲーム開発での例

  • ゲームジャム・プロトタイプ: YAGNI/KISS全振りが正解。ハードコード上等。
  • 運用タイトルのガチャ・課金まわり: 「知識のDRY」は厳守(確率表記の知識が2か所にあると事故が法律問題になる)。
  • 「汎用ステートマシンエンジンを作り始めて2週間、敵は1体もできていない」— ゲーム開発で最も typical なYAGNI違反。

Unity/C# との対応

  • 「どのゲームでも使える俺のフレームワーク」をアセット化してから本体を作り始めるのは早すぎる抽象化の典型。
  • [SerializeField] でインスペクタに出す設定値は、DRYの観点では「知識をデータに一元化」する正しい手段。

Unreal Engine との対応

  • Blueprintで試作 → 固まったらC++化、という工程自体が「3回目まで待つ」の実践です。

よくある誤解

  • 「YAGNI = 設計するな」ではありません。変更しやすいコード(低結合・高凝集)にしておくことと、将来の機能を先に作ることは別です。前者は常に価値があり、後者がYAGNI違反です。
  • 「DRY = コピペ禁止」ではありません。禁止すべきは知識の複製で、字面のコピペが常に悪いわけではありません。

使う場面 / 使わない場面

  • 強く適用する場面: プロトタイプ、仕様が流動的な期間、1人開発。
  • 緩める場面: 変更コストが非対称に大きい決定(データフォーマット、API)、安全・金銭に関わる知識の一元化。

関連項目

理解度チェック

  1. DRYが対象とするのは「コードの重複」ではなく何の重複ですか。
  2. YAGNIを適用してはいけない決定の例を1つ挙げてください。
  3. 「早すぎる最適化を避ける」と「アーキテクチャの性能設計」はどう両立しますか。

演習

自分のプロジェクトから「同じような処理が2か所にあるコード」を1つ見つけ、(a) それは知識の重複か字面の重複か、(b) 統合した場合に生まれる結合、(c) 統合しない場合に起きうる事故、を書き出して統合の是非を判断してください。


前: 設計の品質特性 | カテゴリ目次 | 次: 技術的負債・コードの臭い・リファクタリング