Repository、Serviceクラスの設計をいろんな視点で評価してみる
背景
先週実装したコードで、リポジトリクラスの追加について少々議論がありました。
そんな時に下記記事を拝見したので、それをもとに今回の改修内容の妥当性について整理し、記事としてまとめてみました🐈
今回の改修内容
仕様
-
hogeテーブルのanswerカラムが更新されたら、以下のロジックに沿ってイベントテーブルにレコード追加をする。
answerの値 処理内容 許可A (1) イベントAに「許可」を記録 許可B (2) イベントBに「許可」を記録 拒否A (3) イベントAに「拒否」を記録 拒否B (4) イベントBに「拒否」を記録 -
イベントAテーブル、イベントBテーブルはhogeテーブルに紐づくテーブル。
-
hogeテーブルのanswerカラムはAPI経由でしか更新されない。
元々のコード
元々の実装では、API用のコントローラーがHogeリポジトリの更新メソッドを直接呼び出していました。
// API用のコントローラー
class HogeApiController
{
private HogeRepository $hoge_repository;
public function __construct(HogeRepository $hoge_repository,)
{
$this->hoge_repository = $hoge_repository;
}
public function postEdit(Request $request)
{
// HogeリポジトリのAPI用更新メソッドを呼び出す
$this->hoge_repository->updateByApi($request->input());
}
}
修正したコード
改修後は、HogeServiceクラスを追加し、サービスクラスがリポジトリを呼び出す形に変更しました。
// API用のコントローラー
class HogeApiController
{
private HogeRepository $hoge_repository;
public function __construct(){
}
public function postEdit(Request $request)
{
$hoge_service = app(HogeService::class);
$hoge_service->update($request->input());
}
}
// Hogeデータ用のサービスクラス
class HogeService
{
private HogeRepository $hoge_repository;
private AEvenRepository $a_event_repository;
private BEvenRepository $b_event_repository;
public function __construct(
HogeRepository $hoge_repository,
AEvenRepository $a_event_repository,
BEvenRepository $b_event_repository,
){
$this->hoge_repository = $hoge_repository;
$this->a_event_repository = $a_event_repository;
$this->b_event_repository = $b_event_repository;
}
public function update(array $input)
{
// Hogeの更新処理
$hoge_model = $this->hoge_repository->updateByApi();
// イベント追加処理
$this->createEvent($hoge_model->id, $hoge_model->answer);
}
private function createEvent(int $hoge_id, int $answer)
{
switch ($answer) {
case 1: // 許可A
$this->a_event_repository->create($hoge_id, 1); // 1:許可
break;
case 2: // 許可B
$this->b_event_repository->create($hoge_id, 1); // 1:許可
break;
case 3: // 拒否A
$this->a_event_repository->create($hoge_id, 2); // 2:拒否
break;
case 4: // 拒否B
$this->b_event_repository->create($hoge_id, 2); // 2:拒否
break;
default:
break;
}
}
}
改修内容の評価
1テーブルにつき1リポジトリで良いのか?
今回の改修では結果的に1テーブルにつき1リポジトリの状態になっています。
参照している記事では、つらくなるリポジトリパターンとしてDBテーブルごとにリポジトリが例として出されています。
今回の改修は適切ではなかったのでしょうか🤔
以下の点を考慮して評価してみます。
イベントの種類が増えた場合
イベントテーブルが増えた場合を想定してみます。
例えばanswerがが新たに「許可C」という値を持ち、イベントCテーブルにレコードを追加する仕様になったとします。
その場合、HogeServiceの依存関係にイベントCのリポジトリを追加する必要があります。
このように、イベントの構成変更がサービスクラスに影響を与える設計は妥当なのでしょうか?
これを良しとするかは、イベントテーブルの構成変更がHogeServiceの関心ごとかどうかで判断したいところです。
今回の例では、イベントA/Bテーブルは独立した概念ではなく、hogeテーブルのanswerカラムに基づくイベントを記録するためのものであり、hogeテーブルに密接に関連しています。
そのためイベントテーブルの構成変更はHogeServiceの関心ごととみなしても問題ないと判断しました。
例が抽象的すぎるので、具体的な例で考えてみます。
-
注文テーブル
のステータスカラム更新したときに、注文ステータス変更イベントテーブル
にレコード追加する場合- この場合、注文ステータス変更イベントは「注文」という操作に密接に関係しているため、注文リポジトリ群に含めるのが適切です。
-
注文テーブル
のステータスカラム
を「注文済み」に変更したときに発注テーブル
にレコード追加する場合- 「発注」という操作は「注文」とは独立した概念です。
- そのため、発注リポジトリを注文リポジトリ群に含めるのは適切ではなさそうです。
イベントが別々で登録される場合
現在の仕様では、イベントA/Bリポジトリは同じタイミングでのみ呼ばれます。
常にセットで呼ばれるのであれば、ひとつのリポジトリにまとめる方が良さそうです。
その場合、上記のようにイベントの種類が増えた場合もHogeServiceクラスには影響を与えず、イベントリポジトリのみで完結することができます。
ただ今回イベントA/Bリポジトリを分けたのには事情がありました。
今はhogeテーブルとイベントA/Bテーブルが関連する構成になっているのですが、理想は以下の構成です。
将来的にはイベントA/Bリポジトリが異なるタイミングで呼ばれる可能性もあるため、それぞれのリポジトリを持つことにしました。
ただしイベントの種類が多くなるとHogeServiceの依存関係が増え、可読性が損なわれる可能性があります。
この場合は、イベント操作を1つのリポジトリにまとめるか、リポジトリをさらに抽象化することを検討したいところです。
HogeServiceクラスでビジネスロジックを持つべきか?
「answerの値によってどのイベントにレコードを追加するか」のロジックは、HogeServiceに実装されています。
一方で、各リポジトリは単純なcreate()メソッドを持つだけの構造になっています。
今の構造によって、以下のような課題が考えられます。
- HogeServiceの肥大化
- hogeテーブルは比較的大きいテーブルのため、今後もビジネスルールが増えていくと思われます。
- その結果HogeServiceに多くのロジックが集中し、保守が難しくなる恐れがあります。
- 依存関係の増加
- 関連するテーブルや処理が増えると、HogeServiceで使うリポジトリの数も増えていき、サービスクラスが複雑化する可能性があります。
今回の修正は「API経由でhogeテーブルを更新した際の処理」に限られているため、ユースケースを明確にしたサービスクラスを用意するのが良さそうです。
例えば、「HogeApiService」のようなクラス名にすることで「APIによるhogeテーブルの更新処理」を専任とし、他のユースケースとの混在を防げます。
結論
今回の改修について以下が分かりました。(※あくまで筆者視点の話)
- リポジトリ構成は将来の拡張性を加味したものになっている
- サービスクラスの命名がサービスクラスの肥大化を招きかねないものになっている
補足
1テーブルにつき1リポジトリの構成を推奨したいわけではない
今回は結果的に1テーブルにつき1リポジトリの構成になっていますが、常にそうであるべきと主張したいわけではありません。
1テーブルにつき1リポジトリの構成にしていると、ビジネスロジックの変更を伴わないDB構成変更もサービス層に影響を与えてしまうことがあります。
参照している記事ではusersテーブルをusersテーブル、user_friendsテーブルに分ける変更が、すべてのユースケースクラスに影響しているこが紹介されています。
テーブルの正規化やテーブル分割など、リファクタリング都合のDB構成変更はリポジトリ層で吸収できるような作りにしておきたいところです。
私が所属しているチームではデータモデリングに力を入れ始めているので、そういった変更に強い実装を心がけたいです。
システム全体にDDDを取り入れらているわけではない
今のシステムは元々MVC構成だったため、モデルやサービスにビジネスロジックが集中している状態です。
全体をDDDに置き換えるのは難しく、ところどころ既存の構成に引っ張られた改修となっております。
感想
りぽじとりぱたーんってむじぃ
参考
メモ
読んでおきたい記事
Discussion