初投稿です、よろしくお願いします。
勉強しながらの記事作成なので、間違っている部分や説明不足な点があれば詰めて頂けるとありがたいです。
はじめに
最近設計に関する勉強をしていて、単一責任の原則(Single responsibility principle)について、もう一度考えるという記事と、PHPで考える単一責任の原則という記事がとても勉強になったので、アウトプットのためにPHPのコードで自分なりに説明してみました。
単一責任の原則
要約
- 1クラスの責任は1アクター(利用者)まで
メリット
- 変更に強くなる
- 理解しやすいコードが書ける
- コンフリクトを回避できる
説明
前提知識
アクター
システムの利用者。人や組織、外部システムなど。
ユースケース
システムの利用例。ログインシステムで例えると「ユーザー(アクター)がログインする」のような具体的なアクションを指す。
クラスの責任について
クラスにはアクターが必ず存在する
プログラムは全てアクター(利用者)がシステムを利用する(ユースケース)ことによって何かしらの課題を解決しています。どんなクラスでも必ずアクターが存在し、全く関係のなさそうなクラスでも階層をたどると必ずアクターに繫ります。
上記を踏まえて、単一責任原則ってなんですか
1つのクラスに存在するアクターを1つに絞った方がいいよ!っていう原則。
例
機能例: ログイン機能
アクター: 管理者、一般ユーザー
単一責任になっていないコード例
class User
{
private $email;
private $password;
public function __construct($email, $password)
{
$this->email = $email;
$this->password = $password;
}
public function GeneralUserLogin()
{
// 一般ユーザーログイン処理
}
public function AdminUserLogin()
{
// 管理者用ログイン処理
}
}
上記の例は、ログイン機能の処理としては、管理者も一般ユーザーも同じだから同じクラスにまとめてしまえ!という感じですね。
単一責任の原則でいうとこのUser
というクラスは「管理者」「一般ユーザー」という2つのアクターが存在してしまっているので違反していることになります。
では、単一責任の原則に違反しているとどのような不都合があるのでしょうか?
例えば、管理者が突然「やっぱりログイン機能はメールじゃなくてユーザー名に変えよう!」といった要求を出してきたとします。
class UserLogin
{
// $emailを$user_nameに変更
private $user_name;
private $password;
public function __construct($user_name, $password)
{
$this->user_name = $user_name;
$this->password = $password;
}
public function GeneralUserLogin()
{
// メールとパスワードのままログイン処理したいのに!
}
public function AdminUserLogin()
{
// 管理者用ログイン処理
// ユーザー名とパスワードでログインできるように編集
}
}
なんで今更...?という文句を飲み込みつつ、渋々ログイン方法をメールからユーザー名に変更しAdminUserLogin
メソッドをユーザー名で編集しました。
ですが、GeneralUserLogin
では、メールとパスワードのままログイン処理がしたいので、メソッドに影響が出てきてしまいました。
というように、他のアクターに影響が出ないように1クラス1アクターにしよう!というのが単一責任の原則の内容になります。
単一責任になっているコード例
class AdminUser
{
private $name;
private $password;
public function __construct($name, $email, $password)
{
$this->name = $name;
$this->password = $password;
}
public function login()
{
// 管理者用ログイン処理
}
}
class GeneralUser
{
private $email;
private $password;
public function __construct($email, $password)
{
$this->email = $email;
$this->password = $password;
}
public function login()
{
// ユーザーログイン処理
}
}
ログイン機能を単一責任の原則になるように修正しました。AdminUser
のアクターは管理者、GeneralUser
のアクターは一般ユーザー、のように1クラス1アクターに分けられていますね。
こうすれば、管理者が後で「権限を追加して欲しい!」とか「ログインにIDも追加したい!」とか言われても一般ユーザーには影響が出ないので安心ですね。先に言って欲しいですが。
クラスに対してアクターを一つ割り当てるのは難しいのでは?
クラスに紐づくのはアクターである
といは言っても、実際のコードだと共通化したい部分がありますよね。でも共通化すると、他のクラスに影響してしまうので、単一責任の原則に違反してしまいそうです。
ここで重要なのは、「特定のユーザー」や「特定のシステム」がクラスに紐づく訳ではなく、アクターが紐づく点です。つまり、共通化したクラスに対して、共通化に対応したアクターが存在すれば良い、ということです。
もう一度ログイン例で見直してみます。
例
機能例: ログイン機能
アクター: ユーザー(共通) 、管理者、一般ユーザー
<?php
class User
{
public function login()
{
// 共通ログイン処理
}
public function ... // その他共通処理
}
class AdminUser
{
private $email;
private $password;
public function __construct($email, $password)
{
$this->email = $email;
$this->password = $password;
}
public function login()
{
$user = new User;
$user->login($this->email, $this->password);
}
}
class GeneralUser
{
private $email;
private $password;
public function __construct($email, $password)
{
$this->email = $email;
$this->password = $password;
}
public function login()
{
$user = new User;
$user->login($this->email, $this->password);
}
}
今回の例では、GeneralUser
とAdminUser
のログイン機能をUser
にまとめた形となります。対応した各クラスのアクターは下記になります。
-
GeneralUser
-> 一般ユーザー -
AdminUser
-> 管理者 -
User
-> ユーザー(共通)
変更要求がアクターから出された場合でも、各アクターに対応したクラスの内容を変更すれば良いのでこれで単一責任の原則に則った共通化が実装できました。
アクターの存在が重要
単一責任の原則では、アクターの存在が必ず求められます。
アクターが存在しないということは変更がされないということなので、設計の段階でアクターを決めておき、それに則ってクラスを分割していくことが重要です。
まとめ
・1クラスの責任は1アクター(利用者)まで
・共通化したい場合は、共通化に対応したアクターが存在すれば良い
・設計の段階でアクターを決めておき、それに則ってクラスを分割していく