関心の分離とは、目的や変更理由が違う処理を混ぜず、それぞれ独立して理解・変更できるようにする設計原則です。
一言でいうと
関心の分離は「一緒に変わらないものを一緒に置かない」という考え方です。
たとえば、入力チェック、業務判断、DB保存、画面表示、ログ出力は、それぞれ変更理由が違います。これらを1つの関数に詰め込むと、少しの変更でも全体を壊しやすくなります。
関心とは何か
関心とは、コードが扱っている目的や責務のことです。
| 関心 | 例 |
|---|---|
| 表示 | HTMLを組み立てる、画面状態を出す |
| 入力 | リクエストを受け取る、フォームを読む |
| 業務ルール | 割引計算、承認条件、状態遷移 |
| 永続化 | DBへ保存する、ファイルへ書く |
| 通信 | 外部APIを呼ぶ、メッセージを送る |
| 運用 | ログ、メトリクス、監査 |
関心の分離では、これらをどの単位で分けるかを考えます。
悪い例
async function registerUser(req, res) {
const email = req.body.email;
if (!email.includes("@")) {
console.log("invalid email");
return res.status(400).json({ error: "invalid" });
}
await db.query("insert into users ...");
await mailer.send(email, "welcome");
return res.json({ ok: true });
}
この関数には、HTTP、入力チェック、DB、メール、レスポンス、ログが混ざっています。
分けた例
class RegisterUserUseCase {
constructor(
private readonly users: UserRepository,
private readonly mailer: WelcomeMailer,
) {}
async execute(input: RegisterUserInput): Promise<void> {
const email = EmailAddress.create(input.email);
const user = User.register(email);
await this.users.save(user);
await this.mailer.sendTo(email);
}
}
ControllerはHTTPを扱い、ユースケースは登録の流れを扱い、Repositoryは保存を扱います。
凝集度と結合度
関心の分離は、凝集度と結合度で考えると分かりやすくなります。
| 用語 | 意味 | 目指す方向 |
|---|---|---|
| 凝集度 | 1つの部品の中身がどれだけ同じ目的にまとまっているか | 高くする |
| 結合度 | 部品同士がどれだけ強く依存しているか | 低くする |
良い設計は、関連する処理がまとまり、無関係な処理が離れています。
分けすぎにも注意
関心の分離は、何でも細かく分けるという意味ではありません。
| 分けるべきサイン | 分けすぎのサイン |
|---|---|
| 変更理由が違う | 1行読むために5ファイル移動する |
| テストしたい単位が違う | 名前だけ違う薄い関数が大量にある |
| 再利用したい | 抽象化の目的が説明できない |
| 外部依存を隠したい | 実装より構造が複雑 |
読みやすさと変更しやすさのバランスを取ります。
実務での判断基準
- そのコードは何のために存在するか
- 変更理由を1つに説明できるか
- テストしたい単位と一致しているか
- 外部サービスやDBの都合が混ざっていないか
- 名前が責務を表しているか
名前を付けにくい場合、責務が混ざっている可能性があります。
よくある誤解
ファイルを分ければ関心が分離される
ファイルが分かれていても、互いに内部実装を知りすぎていれば分離できていません。分け方より依存関係が重要です。
共通化すればよい
似ているコードでも、変更理由が違うなら共通化しないほうがよい場合があります。重複よりも誤った抽象化のほうが高くつくことがあります。
最初から完璧に分ける
最初から最適な境界を引くのは難しいです。変更が見えてきた段階で、責務を分け直すのが現実的です。
まとめ
関心の分離は、保守しやすいコードを書くための土台です。
変更理由が違うコードを混ぜないことで、読む範囲、直す範囲、テストする範囲を小さくできます。