とあるテックカンファレンスの翌日
ワイ「昨日の フロッピーディスクカンファレンス in サハラ砂漠,良かったなー」
ワイ「久しぶりのリアルイベントやったけど」
ワイ「つよつよエンジニアさんたちの話がいっぱい聞けて」
ワイ「ワイもちょっと強くなった気がするで...!」
嫁はん「(サハラ砂漠のリアルイベント,どうやって日帰りしたん...?)」
息子「きっとそんな気がするだけだね!」
ワイ「なんやて!」
ワイ「色々学んできたんや!」
息子「たとえば?」
ワイがテックカンファレンスで学んだこと
ワイ「一番印象に残ったのは,アーキテクチャの話や」
ワイ「責務を分割するってやつや」
息子「お,誰もが通る道だね」
ワイ「3歳が何言うてんねん」
ワイ「息子くんはもう通ったんかい」
息子「当たり前でしょ」
息子「で,具体的にはどんな話だったの?」
ワイ「(前世の記憶でも残ってんのかい)」
ワイ「えーとな」
ワイ「Laravel を題材にしてあったんやけど」
ワイ「ふつうに使うと, Model, View, Controller に処理を書くやんか」
息子「いわゆる MVC だね」
ワイ「せや」
ワイ「でも,それだと Controller か Model にビジネスロジックを書くことになって」
ワイ「リクエストの処理や DB とのやりとりと混ざってしまって」
ワイ「一箇所に責務が集中するんや」
息子「ふーん」
ワイ「せやから」
ワイ「Controller → UseCase → Service → Repository という風に」
ワイ「それぞれの層で責務を分割してクリーンにするんや」
ワイ「そして,それぞれコンストラクタから DI して」
ワイ「テストを書きやすくするんや」
ワイ「これが最強のアーキテクチャなんや!」
息子「(うわぁ,なんか色々勘違いしてそう...)」
息子「(検討違いではないんだけど...)」
息子「じゃぁさ,ちょっと Laravel でそれやってみてよ」
ワイ「おう!任せてや!」
ワイ「勉強(半日)の成果見せたるわ!」
クリーン(?)なアーキテクチャを実践する
ワイ「と言ってもやな」
ワイ「実は帰りの飛行機で居ても立っても居られなくて」
ワイ「サンプルコードを実装したんや」
嫁はん「(ワープとかじゃなくてちゃんと飛行機で行ってたんかい)」
息子「お,ちょっと見せてよ」
サンプルコードのお披露目
ワイ「これがまず Controller や」
public function swapUserNames(Request $request, SwapAction $action): JsonResponse
{
$firstUserId = $request->input('first_user_id');
$secondUserId = $request->input('second_user_id');
try {
$action($firstUserId, $secondUserId);
return new JsonResponse([
'message' => '名前を入れ替えました!',
], 200);
} catch (Exception $e) {
return new JsonRespose([
'message' => 'エラーが発生しました',
'details' => $e->getMessage(),
], 500);
}
}
嫁はん「(なんちゅう悪質な API や...)」
ワイ「Controller は HTTP リクエストを受け取るのと,レスポンスを作るだけにして」
ワイ「実際のロジックは UseCase に委譲したんや」
ワイ「そしてこれが UseCase や」
class SwapAction
{
public function __construct(
private readonly UserRepository $repository,
) {
}
public function __invoke(int $firstUserId, int $secondUserId): void
{
$firstUser = $this->repository->findByIdOrFail($firstUserId);
$firstUserName = $firstUser->name;
$secondUser = $this->repository->findByIdOrFail($secondUserId);
$secondUserName = $secondUser->name;
// 一人目と二人目の名前を入れ替える
$this->repository->updateNameById($firstUserId, $secondUserName);
$this->repository->updateNameById($secondUserId, $firstUserName);
return;
}
}
ワイ「今回は Service に切り出すほどのロジックもなかったから」
ワイ「UseCase が直接 Repository に依存するようにして」
ワイ「DB 操作だけをさらに委譲することにしてみたで」
ワイ「あの有名な mpyw はんが提唱している"なんちゃってクリーンアーキテクチャ"でも」
ワイ「UseCase で EloquentModel を使うことを許容していたし」
ワイ「これでええんや」
ワイ「ほな,最後は Repository や」
use App\Models\User;
class UserRepository
{
public function __construct(
private readonly User $model,
) {
}
public function findByIdOrFail(int $id): User
{
return $this->model
->newQuery()
->findOrFail($id);
}
public function updateNameById(int $id, string $name): int
{
return $this->model
->newQuery()
->whereKey($id)
->update(['name' => $name]);
}
}
ワイ「コンストラクタ DI はすべてを解決するからな」
ワイ「Repository には対応する Model を...」
息子「ちょっとまった!」
ワイ「!?」
息子「パパ,無闇に Model を DI しないで?」
ワイ「なんでや...?」
ワイ「依存するクラスは DI しまひょって,カンファレンスで言うてたで?」
ワイ「DI しておくと,差し替えが可能なんやで?」
息子「うーん,ちょっと説明するね」
息子くんが解説してくれるらしい
息子「パパはさ」
息子「Laravel の Eloquent Model って,」
息子「どういう役割だと思う?」
ワイ「どういうって...」
ワイ「DB にデータを取りに行くためのものやろ?」
息子「うーん,ちょっと違うな」
息子「たしかに, User::find(1)
みたいな感じで」
息子「Model を起点にデータアクセスはできるよね」
息子「でもこの操作って実は QueryBuilder に委譲してるの」
息子「パパの Repository の実装だって」
$this->model->newQuery()->findOrFail($id);
息子「ってなってるじゃん?」
息子「まさしく,Model を通して Query Builder を直接呼んでいるよね」
ワイ「確かに...」
ワイ「じゃぁ一体,Model はどういう役割なんや?」
息子「Model はね...」
息子「データの形 や データの操作方法 を表現する役割があるの」
息子「Laravel の Eloquent Model は Active Record パターン を採用していて」
息子「それぞれのレコードをオブジェクトとして扱うことができるようになっているんだよ」
ワイ「Active Record パターン,なんか聞いたことあるで...!」
息子「そうでしょ?」
息子「例えば User の Model があるとして」
息子「それは "ユーザ"という一つのデータの形を表現しているの」
息子「そのユーザがどのような操作ができるのか...」
息子「例えば 削除 だったり, 保存 だったり...」
息子「だから, Model には find()
とか first()
みたいな取得系のメソッドは実装されてないけど」
息子「save()
とか update()
とか delete()
みたいな」
息子「データそのものを操作するメソッドは実装されているよね」
ワイ「確かに...!」
ワイ「言われてみれば,ユーザーのデータありきの操作はできるけど」
ワイ「そうじゃないときは QueryBuilder を使ってまずは取得するようになっとるな...!」
ワイ「すごいな息子くん!」
息子「えっへん」
ワイ「せやけど...」
ワイ「それと, Model を DI したらアカンのと何が関係あるんや?」
息子「えっとね」
息子「さっきも言ったみたいに,Model は 1 つのレコードを 1 つのオブジェクトとして扱うの」
息子「だから,レコードの数だけインスタンスができるの」
息子「要は,ひとつひとつのオブジェクトそのものに意味があるの」
ワイ「せやな」
息子「だからね」
息子「無闇に Model を DI すると」
息子「それがただのデータアクセスの道具として使われてしまうの」
息子「本来の Model の役割が薄れてしまうんだよ」
ワイ「そうか...」
ワイ「ワイも結局, $this->model->newQuery()
みたいな感じで」
ワイ「Model を Query Builder を呼ぶ道具としてしか使ってへんかった」
ワイ「それに $this->model
自身を状態をもった”データ”としては扱わんかった」
ワイ「Model を DI したからと言って絶対バグるというわけではないものの」
ワイ「Model の本来の使い方ではなかったってことやな」
息子「そうだね」
息子「パパが言ってた通り,DI は依存するクラスを差し替え可能にする」
息子「だけど,それは Service や Repository, UseCase のような」
息子「動作が主のクラスに対して有効なんだ」
息子「Model はデータの形を表現するものだから,それを無闇に差し替えることはないんだよ」
息子「パパがカンファレンスで聞いたのは,適切なクラスやインターフェースに対しての DI の話だと思うよ」
ワイ「なるほどな...!」
ワイ「たしかに,そもそも Repository の UnitTest なんてほぼ書かへんしな...」
ワイ「Model をモックしたいっていうこともなさそうやし」
息子「そうだね」
息子「Repository はむしろ DB アクセスはモックしないで」
息子「実際の DB 通信を含んだ(Laravel的な)機能テストに分類すべきだね」
ワイ「めちゃめちゃ良くわかったわ...!」
息子「良かった!」
息子「じゃぁ,ちょっと Repository 書き直してみてよ」
ワイ「せやな」
Repository を修正してみる
ワイ「コンストラクタももう要らんから消してしまって...」
class UserRepository
{
public function findByIdOrFail(int $id): User
{
return User::query()->findOrFail($id);
}
public function updateNameById(int $id, string $name): int
{
return User::query()
->whereKey($id)
->update(['name' => $name]);
}
}
ワイ「どうや!」
息子「うん.いい感じだね!」
息子「実際には Scope とかリレーションとかもあるだろうから」
息子「Model クラスを通してデータアクセスする分には問題ないね」
息子「無駄なプロパティやコンストラクタも減ったし」
息子「すっきりしたね!」
その日の夜
ワイ「いやー,今日も学びが多かったわ!」
ワイ「カンファレンスももちろん良かったけど」
ワイ「家で息子くんに教わるのも悪くないな!」
ワイ「それにしても,なんで息子くんはあんなに知識が多いんや」
ワイ「前世の記憶でも残ってるんかな」
息子「ちなにさ,パパ」
ワイ「なんや?」
息子「パパが行ったイベントって,フロッピーディスクカンファレンス in サハラ砂漠?」
ワイ「せやで」
息子「フロッピーディスクって何?」
ワイ「いやフロッピーは知らんのかい」
ワイ「そこはちゃんと令和ベイビーなんやな」
(おしまい)