LESSON 30分

ストーリー

高橋アーキテクト
SOLIDの最後の2つ、ISPとDIPは密接に関連している。一緒に学ぼう
高橋アーキテクト
ISPは”必要なものだけを要求しろ”、DIPは”具体ではなく抽象に依存しろ”。この2つを組み合わせると、驚くほど柔軟な設計ができる

ISP:インターフェース分離の原則

Interface Segregation Principle: クライアントは自分が使わないメソッドに依存させられるべきではない。

ISP違反の例

// 太ったインターフェース:すべてのワーカーにすべてを要求
interface Worker {
  work(): void;
  eat(): void;
  sleep(): void;
  attendMeeting(): void;
  writeReport(): void;
}

// ロボットワーカーは食事も睡眠もしない
class RobotWorker implements Worker {
  work(): void { console.log('Working...'); }
  eat(): void { throw new Error('Robots do not eat'); }      // 不要
  sleep(): void { throw new Error('Robots do not sleep'); }    // 不要
  attendMeeting(): void { throw new Error('Not applicable'); } // 不要
  writeReport(): void { throw new Error('Not applicable'); }   // 不要
}

ISPを適用した設計

// インターフェースを役割ごとに分離
interface Workable {
  work(): void;
}

interface Feedable {
  eat(): void;
}

interface Sleepable {
  sleep(): void;
}

interface MeetingAttendee {
  attendMeeting(): void;
}

// 人間のワーカー:すべてを実装
class HumanWorker implements Workable, Feedable, Sleepable, MeetingAttendee {
  work(): void { console.log('Working...'); }
  eat(): void { console.log('Eating lunch...'); }
  sleep(): void { console.log('Sleeping...'); }
  attendMeeting(): void { console.log('In meeting...'); }
}

// ロボットワーカー:必要なものだけ実装
class RobotWorker implements Workable {
  work(): void { console.log('Working 24/7...'); }
}

ISPの効果

// 使う側は必要なインターフェースだけに依存
class TaskScheduler {
  // Workable だけを要求 -- RobotWorker でも HumanWorker でもOK
  assignTask(worker: Workable, task: Task): void {
    worker.work();
  }
}

class LunchScheduler {
  // Feedable だけを要求 -- HumanWorker のみ対象
  scheduleLunch(worker: Feedable): void {
    worker.eat();
  }
}

DIP:依存性逆転の原則

Dependency Inversion Principle:

  1. 上位モジュールは下位モジュールに依存してはならない。両者は抽象に依存すべきである。
  2. 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。

DIP違反の例

// 上位モジュールが下位モジュールの具体クラスに直接依存
class MySQLDatabase {
  query(sql: string): any[] {
    // MySQL固有の実装
    return [];
  }
}

class UserService {
  private database: MySQLDatabase; // 具体クラスに依存!

  constructor() {
    this.database = new MySQLDatabase(); // 直接生成!
  }

  getUser(id: string): User {
    const rows = this.database.query(`SELECT * FROM users WHERE id = '${id}'`);
    return rows[0] as User;
  }
}

問題:

  • UserServiceMySQLDatabase に直接依存
  • PostgreSQL に変更するとき UserService も修正が必要
  • テスト時にモックに差し替えられない

DIPを適用した設計

// 抽象(インターフェース)を定義
interface UserRepository {
  findById(id: string): User | null;
  save(user: User): void;
  delete(id: string): void;
}

// 上位モジュール:抽象に依存
class UserService {
  constructor(private userRepository: UserRepository) {} // インターフェースに依存

  getUser(id: string): User {
    const user = this.userRepository.findById(id);
    if (!user) throw new Error(`User not found: ${id}`);
    return user;
  }
}

// 下位モジュール:抽象を実装
class MySQLUserRepository implements UserRepository {
  findById(id: string): User | null { /* MySQL実装 */ return null; }
  save(user: User): void { /* MySQL実装 */ }
  delete(id: string): void { /* MySQL実装 */ }
}

class PostgresUserRepository implements UserRepository {
  findById(id: string): User | null { /* PostgreSQL実装 */ return null; }
  save(user: User): void { /* PostgreSQL実装 */ }
  delete(id: string): void { /* PostgreSQL実装 */ }
}

// テスト用モック
class InMemoryUserRepository implements UserRepository {
  private users: Map<string, User> = new Map();
  findById(id: string): User | null { return this.users.get(id) ?? null; }
  save(user: User): void { this.users.set(user.id, user); }
  delete(id: string): void { this.users.delete(id); }
}

依存の方向の逆転

【DIP適用前】
UserService → MySQLDatabase
(上位が下位の具体に依存)

【DIP適用後】
UserService → UserRepository ← MySQLDatabase
(両方が抽象に依存。下位の依存方向が逆転!)

ISP + DIP の組み合わせ

// ISP:必要な操作だけのインターフェース
interface UserReader {
  findById(id: string): User | null;
  findByEmail(email: string): User | null;
}

interface UserWriter {
  save(user: User): void;
  delete(id: string): void;
}

// DIP:上位モジュールは必要な抽象にだけ依存
class UserQueryService {
  constructor(private reader: UserReader) {} // 読み取りだけに依存

  getUser(id: string): User {
    const user = this.reader.findById(id);
    if (!user) throw new Error(`User not found: ${id}`);
    return user;
  }
}

class UserRegistrationService {
  constructor(
    private reader: UserReader,  // 重複チェックに必要
    private writer: UserWriter   // 保存に必要
  ) {}

  register(email: string, name: string): User {
    const existing = this.reader.findByEmail(email);
    if (existing) throw new Error('Email already registered');
    const user: User = { id: generateId(), email, name };
    this.writer.save(user);
    return user;
  }
}

高橋アーキテクトのアドバイス:

「ISPで”必要な分だけ”のインターフェースを定義し、DIPで”抽象に依存”させる。この組み合わせが柔軟でテストしやすいアーキテクチャの基盤だ」


まとめ

ポイント内容
ISPの定義使わないメソッドへの依存を避ける
DIPの定義具体ではなく抽象に依存する
ISPの効果不要な実装の強制を防ぐ
DIPの効果実装の差し替え・テストが容易になる
組み合わせ小さなインターフェース + 依存性注入 = 柔軟な設計

チェックリスト

  • ISPの定義を理解し、太ったインターフェースを分割できる
  • DIPの定義を理解し、依存の方向を正しく設計できる
  • ISPとDIPを組み合わせた設計ができる

次のステップへ

SOLID原則の5つすべてを学びました。次は演習で実際にSOLID原則を使ってコードをリファクタリングしましょう。


推定読了時間: 30分