# SOLID 単一責任の原則(SRP: Single Responsibility Principle)
今日はクリスマス。街中はイルミネーションに灯られて、サンタクロースを待っている。窓の外は静かに雨が降り続いている。2025 年もあとわずか、もうすぐ終わろうとしている。
この 1 年を振り返ると、AI が劇的に進化し、コードを書く日々の中で、「もう記事を書く必要はないのではないか」と自問自答してきた。複雑に絡み合ったコードを見て、どこから手を付けていいかわからなくなった経験。一つのクラスに様々な責任が混在し、修正するたびに予期せぬ副作用が発生した経験。そんな苦い思い出が、これからの AI 時代にはなくなるかもしれない。それでも、思い出でありながら、今この記事で 2025 年の締めくくりとさせていただきます。
プログラミングとは、単に動くコードを書くことではない。未来の自分や、チームメンバーが読みやすく、理解しやすく、変更しやすいコードを書くこと。そのために、設計原則という指針がある。そして、その中でも最も基本的でありながら、最も重要な原則の一つが、単一責任の原則(SRP: Single Responsibility Principle)である。
単一責任の原則(SRP: Single Responsibility Principle)は、SOLID 原則の最初の原則として、オブジェクト指向プログラミングにおいて重要な設計原則の一つです。雨音を聞きながら、この原則について改めて考えてみたい。
# SOLID 原則とは
SOLID(ソリッド)は、オブジェクト指向設計の 5 つの原則の頭文字を取ったものです。Robert C. Martin(Uncle Bob)によって提唱され、保守性が高く、拡張しやすいコードを書くための指針として広く知られています。
SOLID 原則は以下の 5 つの原則で構成されています:
S - Single Responsibility Principle (SRP): 単一責任の原則
- クラスや関数は一つの責任だけを持つべき
O - Open/Closed Principle (OCP): 開放/閉鎖の原則
- 拡張に対しては開いており、修正に対しては閉じているべき
L - Liskov Substitution Principle (LSP): リスコフの置換原則
- 派生クラスは基底クラスと置き換え可能であるべき
I - Interface Segregation Principle (ISP): インターフェース分離の原則
- クライアントが使用しないメソッドに依存すべきではない
D - Dependency Inversion Principle (DIP): 依存性逆転の原則
- 具象クラスではなく、抽象に依存すべき
# SRP と SOLID 原則の関係
SRP は SOLID 原則の**最初の原則(S)**であり、他の原則の基盤となる重要な原則です。SRP に従うことで:
- OCP(開放/閉鎖の原則): 単一責任を持つクラスは、拡張しやすく、修正の影響範囲が限定的になる
- LSP(リスコフの置換原則): 明確な責任を持つクラスは、置き換えが容易になる
- ISP(インターフェース分離の原則): 単一責任のクラスは、必要最小限のメソッドだけを提供する
- DIP(依存性逆転の原則): 責任が明確なクラスは、抽象化しやすい
つまり、SRP は SOLID 原則の基礎であり、SRP を理解し実践することで、他の SOLID 原則も自然に適用しやすくなります。
Single Responsibility Principle の読み方
- Single Responsibility Principle: シングル・レスポンシビリティ・プリンスィプル
- SRP: エス・アール・ピー
英語では「シングル・レスポンシビリティ・プリンスィプル」と読みます。Responsibility(レスポンシビリティ)は「責任」という意味です。
# SRP の定義
単一責任の原則とは、クラスや関数は変更する理由が一つだけであるべきという原則です。つまり、一つのクラスや関数は一つの責任(役割)だけを持つべきということです。
**Single Responsibility Principle (SRP)**
states that a class or function should have only one reason to change. In other words, a class or function should have only one responsibility or job to do. This principle is one of the five SOLID principles of object-oriented design, introduced by Robert C. Martin (Uncle Bob).
The principle emphasizes that:
- Each class should have a single, well-defined purpose
- A class should have only one reason to be modified
- If a class has multiple responsibilities, it becomes harder to maintain, test, and understand
Robert C. Martin(Uncle Bob)
# なぜ SRP が重要なのか
SRP に従うことで、以下のようなメリットが得られます:
- 保守性の向上: 一つの責任だけを持つため、変更の影響範囲が限定的になる
- 可読性の向上: コードの目的が明確になり、理解しやすくなる
- テスタビリティの向上: 単一の責任をテストしやすくなる
- 再利用性の向上: 小さな単位で再利用できる
- 結合度の低減: 他のクラスへの依存が減る
# 悪い例:複数の責任を持つクラス
以下の例は、ユーザー情報の管理とメール送信という 2 つの責任を持っています:
class User {
private $name;
private $email;
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
}
// ユーザー情報の管理
public function getName() {
return $this->name;
}
public function getEmail() {
return $this->email;
}
// メール送信の責任(SRP違反)
public function sendEmail($subject, $message) {
// メール送信のロジック
mail($this->email, $subject, $message);
}
// データベース保存の責任(SRP違反)
public function save() {
// データベースへの保存ロジック
$db = new Database();
$db->save($this);
}
}
このクラスは以下の理由で変更される可能性があります:
- ユーザー情報の構造が変わる
- メール送信の方法が変わる
- データベースの保存方法が変わる
# 良い例:責任を分離したクラス
責任を分離することで、各クラスが単一の責任だけを持つようになります:
// ユーザー情報の管理のみ
class User {
private $name;
private $email;
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
}
public function getName() {
return $this->name;
}
public function getEmail() {
return $this->email;
}
}
// メール送信の責任
class EmailService {
public function sendEmail($to, $subject, $message) {
// メール送信のロジック
mail($to, $subject, $message);
}
}
// データベース保存の責任
class UserRepository {
public function save(User $user) {
// データベースへの保存ロジック
$db = new Database();
$db->save($user);
}
}
# JavaScript の例
JavaScript でも同様に、関数やクラスに単一の責任を持たせることが重要です:
# 悪い例
// 複数の責任を持つ関数
function processUser(user) {
// バリデーション
if (!user.email || !user.email.includes("@")) {
throw new Error("Invalid email");
}
// データ変換
const formattedUser = {
name: user.name.toUpperCase(),
email: user.email.toLowerCase(),
};
// API送信
fetch("/api/users", {
method: "POST",
body: JSON.stringify(formattedUser),
});
// ログ出力
console.log("User processed:", formattedUser);
}
# 良い例
// バリデーションの責任
function validateUser(user) {
if (!user.email || !user.email.includes("@")) {
throw new Error("Invalid email");
}
return true;
}
// データ変換の責任
function formatUser(user) {
return {
name: user.name.toUpperCase(),
email: user.email.toLowerCase(),
};
}
// API送信の責任
async function saveUser(user) {
const response = await fetch("/api/users", {
method: "POST",
body: JSON.stringify(user),
});
return response.json();
}
// ログ出力の責任
function logUser(user) {
console.log("User processed:", user);
}
// 使用例
async function processUser(user) {
validateUser(user);
const formattedUser = formatUser(user);
await saveUser(formattedUser);
logUser(formattedUser);
}
# SRP を実践するためのガイドライン
- クラス名から責任を推測できるか: クラス名が「〜と〜」となっている場合、複数の責任を持っている可能性がある
- 変更の理由を考える: クラスを変更する理由が複数ある場合、責任を分離することを検討する
- 小さく保つ: クラスや関数が大きくなりすぎている場合、責任が増えている可能性がある
- 凝集度を高める: 関連性の高い機能をまとめ、関連性の低い機能は分離する
# どのくらいの行数で分割すべきか
SRP を実践する際に、「何行になったら分割すべきか」という具体的な基準について説明します。
# 一般的な目安
厳密な行数の基準はありませんが、以下のような目安が参考になります:
- 関数: 20〜50 行を超える場合は分割を検討
- クラス: 200〜300 行を超える場合は分割を検討
- メソッド: 10〜30 行程度に保つのが理想的
ただし、行数だけで判断するのではなく、責任の数で判断することが重要です。
# 行数よりも責任の数が重要
以下の例は、行数は少ないですが、複数の責任を持っています:
// 20行未満だが、複数の責任を持つ(SRP違反)
class User {
public function process($data) {
// バリデーション(責任1)
if (empty($data['name'])) {
throw new Exception('Name required');
}
// データ変換(責任2)
$this->name = strtoupper($data['name']);
// データベース保存(責任3)
$db->save($this);
// メール送信(責任4)
mail($this->email, 'Welcome', 'Hello');
}
}
逆に、以下の例は行数が多いですが、単一の責任(ユーザー情報の管理)を持っています:
// 100行以上だが、単一の責任を持つ(SRP準拠)
class User {
private $id;
private $name;
private $email;
private $phone;
private $address;
// ... 多くのプロパティ
public function __construct($data) {
$this->id = $data['id'];
$this->name = $data['name'];
// ... 多くのプロパティの初期化
}
public function getName() { return $this->name; }
public function getEmail() { return $this->email; }
// ... 多くのゲッター/セッター
}
# 分割を検討すべきサイン
以下のような場合、分割を検討すべきです:
- クラス名に「And」や「Or」が含まれる:
UserAndEmailServiceのような名前は複数の責任を示唆 - メソッドが長すぎる: スクロールしないと全体が見えない場合は長すぎる可能性
- 複数の理由で変更される: 異なる理由でクラスを修正することが多い
- テストが難しい: 単一の責任をテストするのが困難な場合
- 理解が難しい: コードを読んで、何をしているか説明できない場合
# 実践的なアプローチ
- まず責任を確認: 行数ではなく、クラスや関数が持つ責任の数を確認
- 段階的に分割: 一度にすべてを分割せず、必要に応じて段階的に分割
- 過度な分割を避ける: 小さすぎるクラスや関数は逆に複雑になる可能性がある
- チームで基準を決める: プロジェクトやチームの状況に応じて、目安となる行数を決める
# まとめ
- 行数は目安であり、責任の数が最も重要
- 関数は 20〜50 行、クラスは 200〜300 行を超える場合は分割を検討
- 複数の責任を持つ場合は、行数が少なくても分割すべき
- 単一の責任を持つ場合は、行数が多くても問題ない場合がある
# 実装時の注意点
単一責任の原則は、コードの品質を向上させるための基本的な原則。一つのクラスや関数に複数の責任を持たせると、コードが複雑になり、保守が困難になる。責任を明確に分離することで、より理解しやすく、変更しやすいコードを書ける。
ただし、過度に細かく分離しすぎると、逆に複雑になることもあるため、適切なバランスを見つけることが重要。