単一責任の原則は、SOLID原則の1つです。
クラス設計で説明されることが多いですが、初心者はまず関数で考えると理解しやすくなります。
一言でいうと
単一責任とは、1つの関数に「変更される理由」を1つだけ持たせる考え方です。
「1つのことだけをする」とよく言われますが、より実務的には「なぜこの関数を変更するのか」を見ると判断しやすいです。
悪い例
async function registerUser(input: UserInput) {
if (!input.email.includes("@")) {
throw new Error("invalid email");
}
const user = {
email: input.email,
name: input.name.trim(),
};
await db.users.insert(user);
await mailer.send(input.email, "welcome");
return user;
}
この関数には、入力チェック、データ整形、DB保存、メール送信が入っています。
メール文面が変わっても、DB保存方法が変わっても、入力ルールが変わっても、この関数を触ることになります。
分けた例
function validateUserInput(input: UserInput) {
if (!input.email.includes("@")) {
throw new Error("invalid email");
}
}
function createUser(input: UserInput) {
return {
email: input.email,
name: input.name.trim(),
};
}
処理を分けると、それぞれの変更理由が見えやすくなります。
関数を分けるサイン
| サイン | 例 |
|---|---|
| コメントで段落が分かれる | // validate, // save, // send mail |
名前に and が入る | saveUserAndSendMail |
| テストしたい観点が複数ある | 入力チェックとDB保存を別々に確認したい |
| 例外の種類が違う | 入力エラーと通信エラーが混ざる |
| 変更理由が複数ある | 仕様変更と外部サービス変更が混ざる |
これらが見えたら、関数を分ける候補です。
分けすぎにも注意
単一責任は、1行ごとに関数を作るという意味ではありません。
function trimName(name: string) {
return name.trim();
}
このような関数が必ず悪いわけではありませんが、名前を付ける意味が薄いなら、かえって読みづらくなります。
実務での目安
- 関数名が処理内容を説明している
- 入力と出力が分かりやすい
- テスト名を書きやすい
- 変更理由を1つに説明できる
- 外部I/Oと計算処理が混ざりすぎていない
特に、DB、API、ファイル、メールなどの外部I/Oは、純粋な計算処理と分けるとテストしやすくなります。
まとめ
単一責任の原則は、関数にも使える考え方です。
関数を見たときに「この関数は何の理由で変更されるか」を1つに説明できるか確認しましょう。