ゴール:「このクラスは“何だけ”をやるのか?」と説明できるようになる
クラス間の責務分離は、一言でいうと、
「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()
);
TypeScriptUserService は一切変更していません。
「ある変更理由に対して、触るクラスが1つで済む」
これが、責務分離が効いている状態です。
interface と組み合わせると、責務分離はさらに強くなる
「役割」を interface で表現する
さっきの Logger や Notifier は、
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}`);
}
}
TypeScriptUserService は「具体クラス」ではなく「役割(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のような“調整役”から呼び出すとしたらどう書くか
を紙に書き出してみると、
責務分離の感覚がかなりリアルに掴めてきます。
