ストーリー
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:
- 上位モジュールは下位モジュールに依存してはならない。両者は抽象に依存すべきである。
- 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。
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;
}
}
問題:
UserServiceはMySQLDatabaseに直接依存- 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分