設計原則 レッスン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抽象化しすぎることで起こりやすい問題はどれか?