単一責任の原則(Single Responsibility Principle, SRP)とは、ソフトウェア設計の原則であるSOLID原則のひとつです。
SRPでは「クラスは一つの責任だけを持つべきである」と定義されています。
単一責任の原則の目的は大きく3つあります。
1.保守性の向上
変更が必要な時、変更対象が一つの機能に限定されているため、変更が容易になります。
2.理解しやすさ
一つの責任しか持たないクラスは、その目的や機能が明確であり、他の開発者がコードを読む際の理解が容易になります。
3.再利用性の向上
特定の機能に特化しているため、他のプロジェクトやコンテキストで再利用しやすくなります。
例として「給料受領」のユースケースを考えてみましょう。
以下のようなMoneyクラスがあります。
class Money{
private int $value;
private Currency $currency;
public function __construct(int $value, Currency $currency){
if($value < 0){
throw new Exception('Invalid value');
}
$this->value = $value;
$this->currency = $currency;
}
public function value(): int{
return $this->value;
}
public function currency(): Currency{
return $this->currency;
}
public function add(Money $money): Money{
if($this->currency->equals($money->currency())){
return new Money(
$this->value() + $money->value(),
$this->currency
);
}
throw new Exception('Currency mismatch');
}
}
このMoneyクラスは金額と通貨を保持し、金額を加算するaddメソッドを持っています。
Moneyクラスはプロパティを不変にし、addメソッドでは新しいインスタンスを返すことで不変性を保っています。
このMoneyクラスを使用して、「給料受領」を行う場合、以下のように書けます。
// 現在の所持金
$current_money = new Money(1000, new Currency('JPY'));
// 給料
$salary = new Money(3000, new Currency('JPY'));
// 給料が支給された
$current_money = $current_money->add($salary);
このコードでは、現在の所持金と給料を別のMoneyインスタンスで表し、所持金に給料を加算しています。
加算操作はaddメソッドを通じて行われ、このメソッドはMoneyインスタンスを返します。
この新しいインスタンスが$current_moneyに再代入され、結果として所持金が更新されます。
このプロセスは不変性の原則に従っており、元のMoneyインスタンスは変更されずに残ります。
次のコードはどうでしょうか。
// 現在の所持金
$current_money = new Money(1000, new Currency('JPY'));
// 給料
$salary = new Money(3000, new Currency('JPY'));
// 給料が支給された(給料に所持金を加算?)
$current_money = $salary->add($current_money);
先程のコードとの違いは給料の取得処理です。
先程は$current_moneyのaddメソッドを呼び出していましたが、
ここでは$salaryのaddメソッドを呼んでいます。
このコードは正常に動作します。
ですがどうでしょうか。
「給料受領」というユースケースにおいて、正しい動作は「所持金に給料を加算」であり、「給料に所持金が加算」はドメインのルールに反しています。
つまり、この場合エラーになるのが正常ということになります。
どのように変更を加えるとよいでしょうか。
Moneyクラスにメソッドを追加してみます。
class Money{
// 省略
/**
* 給料を受け取る
*
* @param Money $salary 給料
* @return Money 新しい所持金
*/
public function receiveSalary(Money $salary): Money{
return $this->add($salary);
}
}
次に「給料受領」プロセスでこのreceiveSalaryメソッドを呼ぶようにします。
// 給料が支給された
$current_money = $current_money->receiveSalary($salary);
関数名から操作の意味が明確になりました。
ですが問題は解消していません。
先程同様以下のようなコードでも正常に処理が通ってしまいます。
// 給料が支給された(給料が所持金を給料として受け取る?)
$current_money = $salary->receiveSalary($current_money);
また、金額を表すMoneyクラスに給料を受け取るというドメインロジックを定義するのが正しいかと言われるとそうではない気がします。
そこで新たに上位レベルのUserクラスを作成してみます。
Moneyクラスに定義したreceiveSalaryメソッドをUserクラスに移行します。
class User{
private Money $money;
public function __construct(Money $money){
$this->money = $money;
}
/**
* 給料を受け取る
*
* @param Money $salary 給料
* @return Money 新しい所持金
*/
public function receiveSalary(Money $salary): void{
$this->money = $this->money->add($salary);
}
}
UserクラスのreceiveSalaryメソッドではMoneyクラスのaddメソッドを呼び出しています。
Userクラスを使用した「給料受領」プロセスは以下のようになります。
// ユーザー
$user = new User(new Money(1000, new Currency('JPY')));
// 給料
$salary = new Money(3000, new Currency('JPY'));
// 給料が支給された
$user->receiveSalary($salary);
給料を受け取る操作はUserクラスによってカプセル化され、Moneyクラスは金額の計算に専念することができるようになりました。
クラスごとの責任が明確になりました。
また、MoneyクラスはreceiveSalaryメソッドを持たない為、呼び出し元を入れ替えて以下のように記述するとプログラムがエラーを吐くようになります。
// ユーザー
$user = new User(new Money(1000, new Currency('JPY')));
// 給料
$salary = new Money(3000, new Currency('JPY'));
// 給料が支給された(MoneyクラスにreceiveSalaryメソッドはないためエラー)
$salary->receiveSalary($user);
「給料受領」ユースケースを例に単一責任の原則(SRP)を実践してみました。
Moneyクラスは金額を扱う責任を持ち、その操作(加算)を提供しますが、どの金額が所持金でどの金額が給料かというドメインルールは持ちません。
これは、Moneyクラスが単一責任の原則に従っていることを意味します。
また、Moneyクラスは単一の責任を持つため給料受領ユースケース以外にも様々な金銭勘定のユースケースで再利用が可能になります。
一方で、Userクラスはユーザーの所持金の管理を担当し、「給料を受け取る」というビジネスロジックを実装します。Userクラスは所持金に関する操作の責任を持ちますが、金額の具体的な操作方法(加算)についてはMoneyクラスに委ねています。
この分離により、各クラスは自身の責任に集中でき、単一責任の原則に沿っていると言えます。
ソフトウェア開発におけるSRPの適用は、チーム全体の生産性を向上させます。
また、クラスが一つの責任に集中することで、その責任に関連する変更が必要になった時、予期せぬ副作用を心配することなく、安心してコードを変更することができます。