Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
123
Help us understand the problem. What are the problem?

posted at

updated at

Organization

Controller, UseCase, Service (および Model) の役割分担についての考察

この記事について

Laravel Advent Calendar 2020 の21日目の記事です。

以前以下の記事を書いてから、とあるプロジェクトで導入し、そこそこ上手く機能している実感を得たんですが、新しく加わったメンバーに意図をうまく伝えるのが難しかったり、既存のメンバーでも(自分も含めて)役割分担に迷いが生じることがときどきあるので、表題に挙げた4つのクラス分類について、どのように役割分担していくといいか、指針を明確化しておこうという試みです。

Laravel で Request, UseCase, Resource を使いコントロールフローをシンプルにする - Qiita

筆者独自の基準も含まれているため、既存の書籍や資料などと異なる定義や使い方があるかもしれませんのでご了承ください。

また、本記事はほとんど Laravel と関係ありませんが、上記の記事の続きということで Laravel タグも付与しておきます。

はじめに

概要

まずは、4つのクラス分類と主な役割を簡潔にまとめると以下のような感じになるかと思います。

クラス分類と主な役割

  • Controller: リクエストを受け取り UseCase に渡し、戻ってきた結果をレスポンスに変換して返す
  • UseCase: Controller からリクエストを受け取り、その Controller で行いたい処理を行って結果を返す
  • Service: UseCase 内で生成され、部分的な処理を行い結果を返す
  • Model: UseCase または Service 内で生成され、部分的な処理を行い結果を返す

以下、細かい点は各章に記載します。

UseCase と Service の責務を明確に分けるため、ガイドラインをまず先に示します。

UseCase をつくる際のガイドライン

  • 原則的に動詞を想起させる名前にする
  • 公開メソッドはひとつ
  • 継承はしない
  • 状態を持たない
  • Service や Model に対する命令へのワークフローを構築する

例)Service, Model, Event を扱う

// 予約を行う UseCase
class Reserve
{
    public function __construct(ReservationService $reservationService) {...}
    public function invoke(array $parameters)
    {
        $this->prepare($parameters);
        $reservation = $this->reservationService->reserve($parameters);
        ReservationAddition::createFromReservation($reservation);
        event(new ReservationCompleted($reservation));

        return $reservation;
    }
}

おそらくこの使い方がいちばん多いんじゃないかと思います。Service や Model を呼び出して、一連の処理を逐次で実行します。主な役割はワークフローの構築です。

「継承はしない」と書きましたが、複数のアクションメソッドに対するそれぞれの UseCase で共通の処理を使いたいときは、Service にするか、Trait にするか、別の UseCase にして利用しましょう。UseCase をワークフローに注力させることで、なるべく柔軟にロジックを組み立てられるようにしておきたいので、プライベートメソッドも極力つくらないほうがいいように思います。

例)複数の UseCase を扱う

class Reserve
{
    public function __construct(UseCase1 $useCase1, UseCase2 $useCase2, UseCase3 $userCase3) {...}
    public function invoke($parameters)
    {
        [$params1, $params2, $params3] = $this->divideParameters($parameters);
        $this->useCase1->invoke($params1);
        $this->useCase2->invoke($params2);
        $this->useCase3->invoke($params3);
    }
}

大きな処理の場合、他の UseCase を組み合わせてひとつの UseCase となるパターンがありえます。その場合、パラメータの構築とコントロールフローのみに注力します。

Service をつくる際のガイドライン

  • 原則的には動詞を想起させる名前にする
  • 状態を持たない
  • 以下のいずれかに該当する
    • 複数のモデルへの操作を集約する
    • 外部 API (Stripe や GitHub など)や SDK をラップする
    • ファイルのインポート/エクスポートを行う
    • Excel や PDF などを操作する
    • どのモデルにも属さないまとまった処理を行う

例)複数のモデルを扱う

class ReservationService
{
    public function reserve(array $parameters)
    {
        $attributes = $this->extractReservationAttributes($parameters);
        $reservation = Reservation::create($attributes);
        // Reservation が集約ルートにならないような他の Model に対する処理が続く
        return $reservation;
    }
}

ドメイン駆動設計におけるドメインサービスの定義に準拠し、特定のモデルにも属さないようなまとまった処理を担当させます。

例)外部 API をラップする

class PaymentService
{
    public function __construct(StripeApi $api) {...}
    public function pay(User $user, Plan $plan)
    {
        $this->api->charges->create([
            'amount' => $plan->fee,
            'customer_id' => $user->stripe_customer_id,
        ]);
    }
}

外部API を直接利用するのではなく、そのドメインで利用する処理のみをラップしたインタフェースを持たせます。API も Service 化しておけば、テスト時にモックで差し替えて、Service 内にあるドメインロジックをテストしやすくなります。

例)どのモデルにも属さないまとまった処理を行う

class CalculateFee
{
    public function calculate($dependencies): int
    {
        // 計算ロジックが続く
        return $amount;
    }
}

個人的にはこういうのは振る舞いモデル(Behavior Model)として Model 扱いしたいところではあるんですが、状態を持たないのは全部 Service という割り切り方でもいいかな、とも思うので Service に入れておきます。

UseCase と Service との違い

どちらも動詞を想起させる命名となるため、両者の線引は曖昧ですが、大きく違うのは、UseCase は API やアクションメソッドと対になっている、ということです。各 UseCase は単体で機能として完結しますが、Service は単独ではアプリケーションが提供する機能にはなりません。

Controller と UseCase の関係

UseCase の役割は、ほとんど Controller の役割を受け継いだだけなので、一見無駄に思えるかもしれませんが、まぁぶっちゃけそれに反論することは難しいです。特に、シングルアクションコントローラであれば UseCase をつくらずに Controller 内でワークフローを構築しても、やってることはほぼ変わらないので。そこら辺はチームでコンセンサスを取る必要があると思いますが、アクションメソッド内の行数を常に10行程度に保つことができるのは大きいと思うので、強くおすすめはしないですが、アクションメソッド内の行数が長くなったときに、検討してみてください。

public function __invoke(InvokeSomething $useCase, SomeModel $model, Request $request): JsonResource
{
    $this->authorize('invoke', $model);
    $result = $useCase->invoke($request->validated());
    return new ResultResource($result);
}

UseCase はアクションメソッド(ルーティングで指定される、API に割り当てられた公開メソッド)と対になっているはずなので、コンストラクタインジェクションではなくメソッドインジェクションで注入します。

UseCase は公開メソッドをひとつだけ持ちます(基本的には invoke)ので、Controller の中身は基本的にはほぼすべて上記のようになるでしょう。

Controller の処理が十分シンプルな場合は、Service や Model を直接呼んでも構いません。

public function __invoke(Request $request): SomeModel
{
    $this->authorize('create', SomeModel::class);
    return SomeModel::create($request->validated());
}

===== 2020-12-21 20:00 追記ここから =====

もうひとつ、Controller と UseCase との関係でいうと、HTTP の世界の境界である、という点があります。
たとえば、セッションに付随したデータやログイン済みユーザーなどは、UseCase に渡す前に取得して、プリミティブデータやオブジェクトにしてから渡します。例外も、UseCase より先で起こる例外は極力 PHP 組み込みの例外や独自定義の例外で返し、Controller 側でそれらに応じたレスポンスデータを構築するようにします。

try {
    $useCase->invoke();
} catch (SomethingWrongException $e) {
    return response()->json(['message' => $e->getMessage()], /* 適切なHTTPステータスコード */);
    // または HttpException を継承した独自クラスをスロー
    // throw new HttpSomeErrorException('error', $e);
}

まぁ、どのみち、ほとんどのユースケースはウェブの文脈で呼ばれるでしょうから、それほど厳密に境界を引かなくてもいいかな、とも思いますが、理想はリクエストやレスポンス(百歩譲ってレスポンスはあまり重要ではないですが)から切り離されても問題なく一連の処理が実行される状態だと思っていて、そうしておくと、たとえば、tinker から UseCase を叩いてさくっと特定の状態をつくりだすこともできるようになります。中にはパフォーマンス要件だったり、切り離すことでメリットがデメリットを上回る、みたいなシチュエーションもあったりするので、原理主義的にはなりたくないですが、意識として、HTTP の外と内、という感覚は持っておきたいです。

===== 追記ここまで =====

UseCase と Service の関係

UseCase と Service の関係は、平たく言えば全体と部分です。その UseCase 内で使用するすべての Service はコンストラクタインジェクションで注入します。

class InvokeSomething
{
    public function __construct(SomeService $someService) {...}
    public function invoke()
    {
        $this->someService->doSomething();
    }
}

UseCase と Model の関係

Model を DI している例をときどき見るんですが、コレクションなのか単一インスタンスなのか判別がしにくいので、冒頭で言及したプロジェクトでは使っていません。Model は引数で受け取るか、内部で直接利用でいいと思います。

class InvokeSomething
{
    public function invoke(SomeModel $someModel)
    {
        $someModel->save();
        AnotherModel::doSomething();
    }
}

おわりに

冒頭に書いたとおり、現在 UseCase を導入しているチーム向けに書いたものですが、わりと汎用的に使えるんじゃないかという手応えもあるので、公開することにしました。

アドベントカレンダーに急遽空きが出たので、走り書きみたいになってしまいました。これを元にチームでも議論して、いずれもうちょっと洗練させたいと思っています。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
123
Help us understand the problem. What are the problem?