Edited at

Laravel で Service 層を取り入れるときに検討したいこと


この記事について

普段何気なく使っている Service クラス(Service 層)について、書籍を中心にその役割や目的について書かれた資料を読みながら、Laravel 製のウェブアプリケーションに Service 層を取り入れる際の判断材料となるような情報や観点を、提供できればと思います。

以前にも調べたり考えたりしたんですが、そのときは明確な役割や使い方の提案ができなかったので、それの補遺的な位置づけになります。

Laravelでウェブアプリケーションをつくるときのベストプラクティスを探る (9) Service編 - Qiita


Service レイヤーとは

これ


出典: Martin Fowler's Bliki

https://martinfowler.com/eaaCatalog/serviceLayer.html



どんなときに Service 層が必要になるか


Despite their different purposes, these interfaces often need common interactions with the application to access and manipulate its data and invoke its business logic.


そういった異なる目的(訳者注: 上図の Data Loaders、User Interfaces、Integration Gateway の目的)にかかわらず、これらのインタフェースには、データを操作したりビジネスロジックを実行し対するために、アプリケーションとの共通のやりとりが必要になるときがある。

ようするに、UI からだけでなく、CUIのプログラムとか他システムとの連携部分とかで、共通のロジックを呼ぶ必要があるときに、Service レイヤー越しにドメインロジックを処理すると、共通のインタフェースで操作できるようになるよね、ということと理解できます。

たとえば、

// via Web User Interface

class SomeController extends Controller {
public function someAction(Request $request) {
$this->someService->doSomething($request->all());
}
}

// via Command
class SomeCommand extends Command {
public function handle() {
$this->someService->doSomething($this->options());
}
}

// via Another Service
class AnotherService {
public function doSomething() {
$this->someService->doSomething($params);
}
}

このように処理をラップすることで、上記3つのクライアントがそれぞれ、引数の構築だけを担って、実際の処理を Service クラスに移譲することができるようになります。


Service クラスとは


Service には2種類ある


  • Application Service: Application 層にある Service(何の説明にもなってないですが、後述します)

  • Domain Service: Domain 層にある Service(何の(ry


Domain Service の定義

Domain といえば「ドメイン駆動設計」ですね。

「ドメイン駆動設計」では、巻末で以下のように定義されています:


SERVICE An operation offered as an interface that stands alone in the model, with no encapsulated state.

Evans, Eric. Domain-Driven Design


「サービス

モデルの中で独立したインタフェースとして提供される操作、カプセル化された状態を持たない。」

ちょっとこれだけだとよく分からないので、もう少し詳細な説明を探してみます。


When a significant process or transformation in the domain is not a natural responsibility of an ENTITY or VALUE OBJECT, add an operation to the model as a standalone interface declared as a SERVICE.


「ドメインにおける重要な処理や変換が、エンティティや値オブジェクトの自然な責務ではない場合、サービスとして宣言された独立したインタフェースとしての操作をモデルに追加してください。」

さらに、以下のような記述もあります。


Unlike ENTITIES and VALUE OBJECTS, it is defined purely in terms of what it can do for a client.


「エンティティや値オブジェクトとは異なり、純粋に、クライアントに対してなにができるか、という観点でのみ定義されます。」

ドメインとはなにか、というのは下記の記事も参照してください。

ドメインモデル、ドメインロジックとは何かをコードを交えて考えてみる - Qiita


Application Service の定義


It can be harder to distinguish application SERVICES from domain SERVICES. The application layer is responsible for ordering the notification.

Evans, Eric. Domain-Driven Design


「Application Service と Domain Service を区別するのはもう少し難しいかもしれない。Application 層の責務は通知を命令することである。」

この文の前段で、銀行のシステムで口座の残高がある閾値を超えた場合にメールを送信する、というような例が紹介されていて、閾値のチェックは Domain 層、メールの送信を命令するのは Application 層、メールを送信するのは Infrastructure 層、という切り分けであると述べています。

また他の箇所では、


layer. For example, if the banking application can convert and export our transactions into a spreadsheet file for us to analyze, that export is an application SERVICE.

Evans, Eric. Domain-Driven Design


Spreadsheet へのエクスポート機能も Application Service と言っています。

明確な定義はないものの、Infrastructure 層(メールなどの通知系、ファイルやデータベースの操作、など)へ命令を下すのが Application Service である、と言えそうです。


よい Service の特徴



  1. The operation relates to a domain concept that is not a natural part of an ENTITY or VALUE OBJECT.

  2. The interface is defined in terms of other elements of the domain model.

  3. The operation is stateless.

Evans, Eric. Domain-Driven Design



  1. その操作が、エンティティや値オブジェクトの自然な一部でないようなドメインの概念に関連したものである

  2. そのインタフェースが、ドメインモデルの他の要素の観点から定義されている

  3. その操作が、状態を持たない


Laravel における Service

Service Providers のセクションに出てきます。


Your own application, as well as all of Laravel's core services are bootstrapped via service providers.

https://laravel.com/docs/5.8/providers


あまり深くは見ませんが、config/app.php で providers に登録されている様々な Service Provider の中身を見てみると、命名だったり、初期化の仕方だったり、を参考にできるかもしれません(ただし、後述しますが、アプリケーションで Service をつくるときは、必ずしもサービスコンテナに登録する必要はないと考えています)。


Facade は一種の Service である(と言えるのではないか)

そうして見てみると、いくつかの Facade が Service Provider で登録されていることが分かりました。

そうでない Facade についても、たとえば Log の場合、


  1. ウェブアプリケーションフレームワークというドメインにおいて、ログ操作というのは、どのモデルにも属さない独立した操作である

  2. とはいえ、デバッグ、エラー、といった、アプリケーションの状態に関わるインタフェースがある

  3. 状態を持たない

といった特徴があり、よい Service クラスの特徴に合致しています。

「Facade は一種の Service である」と考えると、どういう機能を Service 化するのがいいのか、のヒントになるような気がします。


Application 層と Domain 層の境界


  • Application 層


    • UI

    • Request(アプリケーション外部からの入力パラメータ)

    • Job/Event(アプリケーションの外部に対する通知やコマンド実行)

    • Response(アプリケーション外部への出力パラメータ)

    • Command(php artisan CLI で実行されるコマンド)

    • app/Http, app/Console, Exceptions, Events, Listeners, Jobs, Policies, Providers



  • Domain 層


    • Laravel に依存しない部分、DB に依存しない部分

    • 厳密には上記の定義だが、Eloquent Model (Active Record) はドメインロジックを書く一方で、中で DB に依存した処理が含まれるものもあり、境界はあいまい

    • Laravel ではどこに配置するかは決まっていない

    • app 以下じゃなくてもいい



  • Service 層


    • Application 層と Domain 層の間に位置し、Application Service はアプリケーション層に、Domain Service は Domain 層に配置する



  • Infrastructure 層


    • DB

    • Mail

    • Notification (mail, slack, etc.)

    • Queue (database, redis, etc.)




Application Service の例


例1: Excel への Export

なんらかのデータを整形して Excel 形式のファイルに出力してダウンロードするというのは、Service 化がもっとも適した例だと思います。

$activeUsers = User::active()->get();

// 出力形式の他にテンプレートファイルを渡すのもいいかもしれません
$exporter = Factory::factory(ExportType::Excel);
// ファイルに保存するなら
$exporter->toFile($activeUsers, $dir, $name);
// Response として返すなら
$exporter->toStream($activeUsers);


例2: 複数のエンティティを一度に更新するようなケース

たとえば UI の都合で、複数のエンティティを一度に更新するといったケースでは、Controller から直接操作するよりも、Service を挟んだほうがいい気がします(UI が頻繁に変わる場合でも、Controller に影響が及ばないようにできます)。

エンティティを個別に操作するパターン

class SomeController {

public function update(UpdateRequest $request) {
$someModel->update($request->someAttributes());
$otherModel->update($request->otherAttributes());
}
}

Serivce を介して更新するパターン

class SomeController {

public function update(UpdateRequest $request) {
$this->updateSomeServce->update($request->validated());
}
}


Domain Service の例


例1: ECサイトにおける Recommendation

Recommend されるのは「商品」だが、「商品」に「レコメンド」の責務を持たせるのは不自然かな、と思ったら、Service クラスに切り出すことができるでしょう。

「おすすめ」インタフェースに対して複数の実装を持たせ、内部のアルゴリズムをインタフェース越しに差し替えることもできます。

class RecommendController {

public function __invoke() {
$user = Auth::user();
return $this->recommender->recommendTo($user); // Userに合わせたレコメンド
}
}


例2: ホテルの予約サービスにおける Reservation

「予約」というエンティティがあるにはあるが、「予約」という振る舞いに対して、内部で複数のエンティティを連動して操作しなければいけないときに、Service クラスに切り出すことができるでしょう。

ロジックが巨大で複雑になるようなら、さらに「空き状況確認サービス」などといった処理ごとに別の Service に分解してもいいかもしれません。

class Reservation {

public function invoke(Accommodation $accommodation) {
if ($this->vacancyFinder->vacant($accommodation)) {
throw new AlreadyOccupiedException('…');
}
// ...
}
}


Service 層を取り入れる際に検討したいこと


振る舞いを Entity に持たせるか Service に持たせるか

Service を使ったバージョン

class ReservationService {

public function cancel(Reservation $reservation) {
$reservation->is_cancelled = true;
$reservation->save();
}
}
// Client
$this->reservationService->cancel($reservation);

エンティティにメソッドを持たせず、プロパティを直接変更しています。

Entity を使ったバージョン

class Reservation {

public function cancel() {
$this->is_cancelled = true;
$this->save();
}
}
// Client
$reservation->cancel();

こちらは、プロパティの変更はエンティティ内で行い、クライアントはエンティティのメソッドを呼び出しています。

「キャンセル」という操作と、実際に更新されるデータが分離されているので、仮にデータ構造が変わり、更新する対象データが変わっても、クライアントに影響が及ぶことがありません( is_cancelled の代わりに cancelled_at にキャンセル日時を入れるようにした場合、とか)


委譲するかしないか

Service を使ったバージョン

class SomeService {

public function create(SomeModel $model) {
return $this->repository->create($model);
}
public function update(SomeModel $model) {
return $this->repository->update($model);
}
}

いわゆる Repository パターンを使った、Controller -> Service -> Repository という流れでのデータ操作の方法です。

Entity を使ったバージョン

$someModel->create($attributes);

$someModel->update($attributes);

単純に委譲しているだけなら直接 Model を書き換えることを検討してもいいと思います(無理に Service 層と Repository を経由する必要はないと思います)。


関連する操作をまとめるか分けるか

決済関連のメソッドをまとめるパターン

class PaymentService {

public function doPayment() {}
public function doRefund() {}
}

Service には状態がないので、いくつ公開メソッドをつくっても基本的には問題にはならないです。

1クラス1メソッドのパターン

class PaymentService {

public function invoke() {}
}

class RefundService {
public function invoke() {}
}

しかし、個々のメソッドの行数が長くなって見通しが悪くなってきた場合、クラスを分けてしまったほうがいいと思います。


まとめ

Service とは何かというところから、実際に Laravel にどう組み込んで使うかというところまで、ざっと見てみましたが、どのようなケースで Service 化するといいか、というのが少し見えてきた気がします。

他にもこういう処理は Service 化するといい、などの提案や、文中の定義や例の部分で改善点みたいなのがありましたら、ぜひコメント欄にてご指摘ください :bow: