TypeScript | 関数・クラス・ジェネリクス:クラス設計 – クラス間の責務分離

TypeScript TypeScript
スポンサーリンク

ゴール:「このクラスは“何だけ”をやるのか?」と説明できるようになる

クラス間の責務分離は、一言でいうと、

「1つのクラスに“何でもかんでも”やらせず、“役割ごと”にクラスを分けること」

です。

ここができていないと、

  • どこを直せばいいか分からない
  • 1行直しただけなのに、別のところが壊れる
  • テストしづらい・再利用しづらい

という“じわじわ効いてくるつらさ”が出てきます。

逆に、責務分離ができていると、

「このクラスは何をするためのものか?」を
一言で説明できるようになります。


まずは「ダメな例」から:何でも屋クラス

典型的な「責務がごちゃ混ぜ」なクラス

よくある“悪い例”から見てみます。

class UserService {
  private users: { id: number; name: string }[] = [];

  createUser(name: string): void {
    const id = this.users.length + 1;
    this.users.push({ id, name });

    // ログ出力
    console.log(`[LOG] ユーザー作成: ${name}`);

    // ファイル保存(っぽい処理)
    console.log("ファイルに保存しました");

    // メール送信(っぽい処理)
    console.log("管理者にメールを送りました");
  }

  getUsers(): { id: number; name: string }[] {
    return this.users;
  }
}
TypeScript

この UserService は、何をしているでしょう?

  • ユーザーをメモリに保持している
  • ログを出している
  • ファイル保存している(ことになっている)
  • メールも送っている(ことになっている)

つまり、「ユーザー管理」「ログ」「永続化」「通知」が全部くっついています。

こうなると、

  • ログの仕様を変えたい
  • 保存先を DB に変えたい
  • メール送信をやめたい

といったときに、全部 UserService を触らないといけません。

「1つのクラスが、複数の理由で変更される」
これが、責務分離ができていないサインです。


責務分離の基本の考え方:「理由ごとにクラスを分ける」

「何のために変更されるクラスか?」で切り分ける

責務分離の有名な言い方に、

「クラスは、1つの理由でしか変更されないようにする」

というものがあります。

さっきの例でいうと、

  • ユーザー管理の仕様が変わった
  • ログの出し方が変わった
  • 保存方法が変わった
  • 通知の仕様が変わった

これらは全部「別の理由」です。

だから、本来はクラスも分かれているべきです。

  • ユーザーを管理するクラス
  • ログを出すクラス
  • 保存するクラス
  • 通知するクラス

というふうに、「役割ごと」に分けていきます。


責務を分けた設計に書き直してみる

役割ごとにクラスを分ける

先ほどの UserService を、責務ごとに分解してみます。

type User = { id: number; name: string };

class UserRepository {
  private users: User[] = [];

  add(user: User): void {
    this.users.push(user);
  }

  findAll(): User[] {
    return this.users;
  }
}

class Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}

class Notifier {
  notify(message: string): void {
    console.log(`通知: ${message}`);
  }
}
TypeScript

ここまでで、

  • ユーザーの保存・取得 → UserRepository
  • ログ出力 → Logger
  • 通知 → Notifier

というふうに、役割ごとにクラスを分けました。

では、UserService はどうなるか。

class UserService {
  constructor(
    private repo: UserRepository,
    private logger: Logger,
    private notifier: Notifier
  ) {}

  createUser(name: string): void {
    const id = this.repo.findAll().length + 1;
    const user: User = { id, name };

    this.repo.add(user);
    this.logger.log(`ユーザー作成: ${name}`);
    this.notifier.notify(`新しいユーザー: ${name}`);
  }

  getUsers(): User[] {
    return this.repo.findAll();
  }
}
TypeScript

ここでの UserService の責務は、かなりはっきりしました。

「ユーザーを作る“手続き”をまとめるクラス」

です。

保存の仕方・ログの出し方・通知の仕方は、
それぞれ専用のクラスに任せています。

何がうれしいかを具体的に感じる

例えば、「ログの出し方を変えたい」とします。

前の“何でも屋”版では、UserService を直接書き換える必要がありました。

責務分離した版では、Logger だけを差し替えれば済みます。

class FileLogger extends Logger {
  log(message: string): void {
    console.log(`[FILE LOG] ${message}`);
  }
}

const service = new UserService(
  new UserRepository(),
  new FileLogger(),
  new Notifier()
);
TypeScript

UserService は一切変更していません。

「ある変更理由に対して、触るクラスが1つで済む」
これが、責務分離が効いている状態です。


interface と組み合わせると、責務分離はさらに強くなる

「役割」を interface で表現する

さっきの LoggerNotifier は、
interface にしておくともっと柔らかくなります。

interface ILogger {
  log(message: string): void;
}

interface INotifier {
  notify(message: string): void;
}

interface IUserRepository {
  add(user: User): void;
  findAll(): User[];
}
TypeScript

これをクラスに implements させます。

class UserRepository implements IUserRepository {
  private users: User[] = [];

  add(user: User): void {
    this.users.push(user);
  }

  findAll(): User[] {
    return this.users;
  }
}

class ConsoleLogger implements ILogger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}

class ConsoleNotifier implements INotifier {
  notify(message: string): void {
    console.log(`通知: ${message}`);
  }
}
TypeScript

UserService は「具体クラス」ではなく「役割(interface)」に依存します。

class UserService {
  constructor(
    private repo: IUserRepository,
    private logger: ILogger,
    private notifier: INotifier
  ) {}

  createUser(name: string): void {
    const id = this.repo.findAll().length + 1;
    const user: User = { id, name };

    this.repo.add(user);
    this.logger.log(`ユーザー作成: ${name}`);
    this.notifier.notify(`新しいユーザー: ${name}`);
  }
}
TypeScript

こうすると、

  • 保存方法を変えたい → IUserRepository を実装した別クラスを渡す
  • ログの出し方を変えたい → ILogger を実装した別クラスを渡す
  • 通知方法を変えたい → INotifier を実装した別クラスを渡す

というふうに、差し替えがさらに簡単になります。

責務分離+interface は、

「役割ごとにクラスを分け、その役割を interface で表現し、
クラス同士を“役割”ベースでつなぐ」

という設計になります。


責務分離ができているかをチェックする質問

自分にこう問いかけてみる

今書いているクラスに対して、
こんな質問を投げてみてください。

「このクラスは、何をするクラスですか?」

それを、
1文で・“そして”を使わずに説明できますか?

例えば、

「ユーザーを保存して、ログを出して、メールも送るクラスです」

となってしまったら、それはもう怪しいです。

理想は、

「ユーザーを保存するクラスです」
「ログを出すクラスです」
「通知を送るクラスです」

のように、「〜するクラスです」が1つに絞れることです。

変更理由の数を数えてみる

もう一つの視点は、

「このクラスが変更される“理由”はいくつあるか?」

です。

  • ビジネスルールが変わったから
  • ログの仕様が変わったから
  • 保存先が変わったから
  • 通知方法が変わったから

こういう“別々の理由”で同じクラスを何度も触るなら、
責務が混ざっている可能性が高いです。

「1つのクラスは、1つの理由でしか変更されない」
これを目標に、クラスを分けていく感覚を持ってみてください。


まとめ:クラス間の責務分離を自分の言葉で整理すると

最後に、あなた自身の言葉でこうまとめてみてください。

  • 責務分離とは、「1つのクラスに何でもやらせず、役割ごとにクラスを分けること」
  • 良いクラスは「このクラスは〇〇をするクラスです」と1文で説明できる
  • 「1つのクラスが、複数の理由で変更される」状態は危険信号
  • 保存・ログ・通知などは、それぞれ専用クラスに切り出す
  • interface と組み合わせると、「役割ごとにクラスを差し替えやすい設計」になる

今のあなたのコードの中から、
「明らかに“何でも屋”になっているクラス」を1つだけ選んでみてください。

そして、

  • そのクラスがやっていることを箇条書きにして
  • それぞれを「〜するクラス」として別クラスに分けるとしたらどうなるか
  • そのクラスたちを、UserService のような“調整役”から呼び出すとしたらどう書くか

を紙に書き出してみると、
責務分離の感覚がかなりリアルに掴めてきます。

タイトルとURLをコピーしました