設計原則 レッスン6

抽象化と境界設計

適切な抽象化のレベルを見極め、モジュール間の境界とインターフェースを設計する考え方を学ぶ

適切な抽象化レベルとは

抽象化は「詳細を隠して、本質だけを見せる」ための手段ですが、 抽象化のレベルを誤ると、かえってコードを理解しにくくします。 抽象化しすぎと、抽象化しなさすぎの両方に問題があります。

抽象化しすぎ

  • • 実装が1つしかないのに何段階もインターフェースを重ねる
  • • 汎用的すぎて、結局何をするクラスなのか名前から読み取れない
  • • コードを追うために何ファイルも行き来する必要がある

抽象化しなさすぎ

  • • 具体的な実装の詳細(SQL文やHTTPの詳細など)が、あらゆる場所に露出している
  • • 同じような処理があちこちにベタ書きされ、変更のたびに全箇所を修正する

ポイント

「実装が2つ以上必要になった時に抽象化する」という経験則は、 抽象化しすぎを防ぐ実践的な目安としてよく使われます(YAGNIとも関連する考え方です)。

モジュール間の境界の引き方

境界を引く際に有効な視点の1つが、「何が一緒に変わりやすいか」で分割することです。 同じ理由で一緒に変更されることが多い処理は同じモジュールにまとめ、 変更理由が異なるものは別のモジュールに分けます。

境界の引き方の例: ECサイトの注文ドメイン

Order(注文)モジュール
  - 注文の作成、注文ステータスの遷移ルール
  - 注文金額の計算ロジック

Payment(決済)モジュール
  - 決済手段ごとの処理(クレジットカード、コンビニ払い)
  - 決済プロバイダとの通信詳細

Shipping(配送)モジュール
  - 配送業者の選定ロジック
  - 配送状況の追跡

→ 「決済方法が増える」変更は Payment モジュール内で完結し、
   Order や Shipping のコードに影響しない境界になっている

インターフェース設計: 最小限の公開面積

モジュールの境界を引いたら、外部に公開するインターフェース(公開API)は必要最小限にとどめるべきです。 公開する情報が多いほど、内部の実装を自由に変更できる余地が狭くなります。

// 悪い例: 内部のデータ構造をそのまま公開してしまう
class ShoppingCart {
  items: CartItemInternal[] // 呼び出し側が内部配列を直接操作できてしまう
}

// 良い例: 必要な操作だけを公開し、内部構造は隠す
class ShoppingCart {
  private items: CartItemInternal[] = []

  addItem(item: CartItem) { /* ... */ }
  removeItem(itemId: string) { /* ... */ }
  getTotal(): number { /* ... */ }
  // 内部のitems配列そのものは公開しない
}

リーキーアブストラクションを避ける

リーキーアブストラクション(抽象化の漏れ)とは、 抽象化によって隠したはずの実装の詳細が、インターフェース越しに利用者側へ 意図せず露出してしまう状態です。

// リーキーアブストラクションの例
interface UserRepository {
  // 戻り値の型がSQLクライアント固有のResultRow型になっている
  // 呼び出し側がSQL実装の詳細を知らないと使いこなせない
  findById(id: string): SqlResultRow
}

// 改善例: ドメインの言葉で表現された型を返す
interface UserRepository {
  findById(id: string): User
}

インターフェースを設計するときは、「利用者がこのインターフェースの向こう側にある 実装技術(SQL・HTTP・ファイル形式など)を知らなくても使えるか」を確認する習慣をつけましょう。

ポイント

  • • 抽象化はしすぎても、しなさすぎても理解しにくいコードになる
  • • 「実装が2つ以上必要になったら抽象化する」は抽象化しすぎを防ぐ目安になる
  • • モジュールの境界は「何が一緒に変わりやすいか」を基準に引く
  • • 公開インターフェースは必要最小限にし、内部のデータ構造をそのまま公開しない
  • • リーキーアブストラクションを避け、利用者が実装技術を知らずに使えるインターフェースを設計する

確認クイズ

1 / 3

抽象化しすぎることで起こりやすい問題はどれか?