2025-10-13

MVCでええやん

フルスタックフレームワークでMVCのアーキテクチャを採用しているものは多い。 一方でレイヤード?ヘキサゴナル?オニオン?アーキテクチャを採用しているサービスも増えているように思う。

個人的にはWebアプリケーションのほとんどのケースはMVCパターンで十分だと思っているのだが、今回はこのあたりの話を書いていく。なお、PHP/Laravelを例として記述していく。

MVCを使うときの大まかな設計の流れ

実装内容の難しさによってどういった構成に着地するか変わってくるが、概ねこんな感じで考えている。

基本形(バリデーション+保存)

管理画面のマスタ管理など純粋なデータの作成・更新で使うようなパターン。

class UserRegistrationController
{
    public function store(Request $request)
    {
        $attributes = $request->validate([
            'name' => 'required|string|max:255',
            // ...
        ]);
        $user = User::create($attributes);
        return redirect()->route('users.show', $user);
    }
}

このパターンに収まるケースはそこまで多くないので、以降でロジックが増えてきたときのパターンでの構成を書いていく。

バリデーションが増えてきたら…

FormRequestクラスにバリデーション処理を移行する。

class UserRegistrationController
{
    public function store(UserRegistrationRequest $request)
    {
        $user = User::create($request->validated());
        return redirect()->route('users.show', $user);
    }
}

保存時の変換ロジックなどが増えてきたら…

Modelにメソッドを生やす

class UserRegistrationController
{
    public function store(UserRegistrationRequest $request)
    {
        $user = User::register($request->validated());
        return redirect()->route('users.show', $user);
    }
}

このModelのユースケースが限られている場合は create 系のメソッドも増えないため、Modelに実装しちゃっても良いというケース。

もっと固有のロジックが増えてきてModelが太ってきたら…

FormクラスやWebのアクションに対して別のEntityを見出してクラス化し、そこにロジックを入れる。

class UserRegistrationController
{
    public function store(UserRegistrationRequest $request)
    {
        $form = new UserRegistration($request->validated());
        $user = $form->save();
        return redirect()->route('users.show', $user);
    }
}

このFormクラスに該当するクラスをUsecaseクラスやServiceクラスとして定義して利用しているプロジェクトもあると思う。ただ、ServiceクラスもUsecaseクラスも曖昧なまま利用されがちなので、利用する場合はチーム内でよく話し合ったほうが良い。例えばコントローラーから複数のServiceを呼び出したり、ServiceがServiceを呼び出したり、あるいは外部サービスに対するアクセス層としてServiceを定義したりするケースもある。そうすると、どこに記述すればよいのか、どこを読めばよいのか曖昧になり保守性が下がる。

純粋なモデリング=Entityを見出すことができるのであればModelを定義してそこから依存するModelを呼び出したり外部アプリケーションへの通信(メールやHTTP APIなど)をすれば良い。

ちなみにFormクラス相当を作る場合でも↓こんな感じでModelディレクトリに切るのが好きです。

app/Models/User/Registration.php

Fat Controllerでもええやん

感覚的に100行くらいの処理であればModelにロジックを切らなくてもトランザクションスクリプトな感じで実装してもあまり問題ないように思う。
テストが書かれていればリファクタリングも容易だし、100行くらいであればあちこちファイルを移動する手間もなくなり保守性としても良くなりそう。

過度にFat Controllerを怖れる必要はない。最初はFat Controllerで保守性厳しいなぁと思ったら適宜切り出せば良い。
同様にFat Modelの方もまずは一旦単一のModelに入れてみて、保守性が悪くなってきたと感じたタイミングでFormや他のModelクラスに切り出せば良い。保守性が悪くなることにはドメイン理解も進んでいるはずなので状況的にも切り出しやすくなっているように思う。

いずれにしてもコントローラをテストできるFeatureテストの実装はとても大事。LaravelはFactoryやモックあたりも含めてとてもテストが書きやすいので正常系だけでも書いておくと良い。

そのデータソース切り替えは本当にあるのか?

Repositoryで層を切っているとデータソースの切り替えがしやすいよね、と言われることがある。たしかにRepositoryの中のコードをいじれば二重書きなどができるようになるので変更は楽なように思える。

一応、大抵のMVCフレームワークは設定ベースでデータソースを切り替えることができる。システム移行など二重書きが必要な場合はアプリケーションコードの変更が必要だが、その場合も二重書きをするクラスに単純置換したりASTなどである程度機械的にできるので、あまり心配ないかもしれないし、労力はRepositoryのときとそこまで変わらないかもしれない。システム移行って結局検証に多大な工数がかかるんですよね…。

無駄に層を入れる必要はない

軽量DDDだとUsecase, Repository, Entity, ValueObjectなどのクラス群を使っている印象がある。RepositoryはActiveRecordを直接使えば良いし、ValueObjectやEntityは普通にModelで良い。UsecaseもModel(Form含む)で十分。無駄に層を増やす必要はない。Modelにロジックを集約させればModelクラスを追うだけで良くなる。前述の通りFat Modelになってきたら別のModelに切り出して移譲すれば良いだけ。

ViewModelとかは必要に応じて入れても良いかもしれないけど、いずれにしてもMVCをベースにして漸進的に考えていければ良い。最初はコントローラにロジックやマッピングベタ書きでも適切なテストが書かれていれば悪いコードじゃないですよ、というのが伝えたい。

ということでMVCでええやん?