バーガーショップで例えるオールアバウトでのLaravelアーキテクチャ
オールアバウトで開発チームに所属している@pakkunです。
12月も近くなり、大きく時期から外れてしまいますが、弊社では8月から9月にかけてサマーインターンを行いました。 その際に弊社で導入しているLaravelというPHPフレームワークの付き合い方を資料とライブコーディングでインターン生に説明しました。
抜粋になりますが、弊社でのLaravelとの付き合い方をブログでも公開します。
とは言え、Laravelを知らない人もいるかと思いますので、まず初めに軽く説明します。
3行でLaravelを知る
- PHPで書かれたフルスタックフレームワーク。
- MVCベース。
- PHP界隈ですごく流行っている。
MVCベースと記載しましたが、開発者のTaylor Otwellさんは「MVC Is Killing You」と著書で言っており、MVCに縛られると辛くなるので、あまり深くとらわれないようにしましょう。
次にどんな機能があるのか見てみましょう。
Laravelの機能
- オブジェクト指向(Laravel5.3ならphp5.6以上)
- バンドル機能(Composerによる依存管理)
- ORM(オブジェクト関係マッピング)
- Restful Controller
- 強力なDependency Injection
- マイグレーション機能
- テスト標準サポート
上記にあげたのは一例です。フルスタックフレームワークであるだけに、他にもイベント管理や認証管理など素晴らしい機能がたくさんあります。
Laravelの基本的なアーキテクチャ
Laravelで必要最低限の機能でWebアプリケーションを実装した場合、下記のようなアーキテクチャになります。
Laravelのアーキテクチャ自体は、ORMがメインになるので、ドメイン駆動設計(DDD)のレイヤー化アーキテクチャにあるドメイン層をベースにして考えると理解しやすいと思います。
Model部分が、Active Recordパターンで実装されており、Repository、Factory、Model(Entity)が組み合わさった状態になっているので、注意しましょう。
ORMを使用するだけで、レイヤー化アーキテクチャのドメイン層の思想に触れられるのは本当に素晴らしいですね。 これをきっかけに、ドメイン駆動設計(DDD)のレイヤー化アーキテクチャを勉強してみても良いと思います。
オールアバウトでのLaravelのアーキテクチャ
Laravelについて簡単に説明しましたので、ここからオールアバウトでのLaravelのアーキテクチャを説明していきます。 先に記載したLaravelの基本的なアーキテクチャを下記のように変更しています。
Serviceというレイヤーが追加されただけなので、基本的なLaravelのアーキテクチャとあまり変わりませんが、重要な役割を担います。
オールアバウトでは、コントローラーやモデルで扱うには不自然な処理であるEager Loading、DBトランザクション、Cacheの処理をServiceで処理するようにしています。
このアーキテクチャをベースにオールアバウトでは開発を行っています。 レイヤー化アーキテクチャを学んでいると、「ドメインサービスでは?」と思う人もいるかと思いますが、ここで定義しているサービスは「アプリケーションサービス」になりますので注意しましょう。
また規模にもよりますが、Serviceでキャッシュを多用する大規模な案件の場合、オブジェクトのライフサイクルが管理しづらくなるので、サービスを「クエリ用サービス」と「コマンド用サービス」に分けるとシンプルになります。 この思想は、「Tell, Don't Ask」、「コマンドとクエリの分離原則」を知っていれば理解できると思います。
Serviceを追加した弊社のアーキテクチャを詳しく説明しますが、普通にアーキテクチャの役割を説明してもわかりづらいので、ファーストフードのハンバーガーショップを例えにして説明していきます。 また、説明するのはデータの取得についてです。データの更新については、後述の「補足」で少しだけ触れます。
まず、Laravelのアーキテクチャを一度忘れてください。 ハンバーガーショップでいつも行っているハンバーガーの買い方を思い出しましょう。
ハンバーガーショップで行っているハンバーガーの買い方
- お店に入る。
- カウンタースタッフに向かう。
- カウンタースタッフに注文をする。
- カウンタースタッフが棚からハンバーガーを持ってくる。
- カウンタースタッフがハンバーガーをトレイに乗せて渡してくれる。
- ハンバーガーをゲット。
上記のような流れになると思います。 もし、「違う」という人がいたら、ファーストフードのハンバーガーショップに行って体験してきましょう。
イメージできた人は先へ進みましょう。 ハンバーガーの買い方を弊社のアーキテクチャで表現してみます。
ハンバーガーの中に人が含まれてしまっていますが、私たちが実際にハンバーガーを買うときにも、作っているスタッフと会うことはないので、隠ぺいされていると考えましょう。
次に弊社のアーキテクチャとハンバーガーショップの役割を比較してみましょう。
オールアバウトのLaravel | ハンバーガーショップ |
---|---|
Route | 入店 / ドライブスルー |
Request | 注文 |
Controller | カウンタースタッフ |
Service | 棚 |
Repository | 置くスタッフ |
Factory | 作るスタッフ |
Model | ハンバーガー |
View | トレイ・紙袋 |
だいぶイメージがしやすくなったのではないでしょうか。
イメージができたところで、ハンバーガーショップを例にして、弊社のアーキテクチャを解説していきます。
ハンバーガーショップで例える弊社のアーキテクチャの役割
Route
Routeは、専用のControllerへ誘導するのが仕事です。それ以上のことはしません。
ハンバーガーショップでは、「入口」にあたります。 ハンバーガーショップでこの役割を説明してみましょう。
- お店の入り口は、店内のカウンタースタッフへの誘導するのが仕事です。
- ドライブスルーの入り口は、ドライブスルーのカウンタースタッフへ誘導するのが仕事です。
ハンバーガーショップで人と車が一緒に並ぶことはないのと同じようにプログラムの設計にも気を付けたいところですね。
Request
Requestは、Controllerが理解しやすいリクエストを作るのが仕事です。
Laravelでは、バリデーションを行う重要なクラスです。
Requestクラスはカスタムで定義できますので、リクエストにビジネスロジックが入ってしまった場合、カスタムで定義したRequestクラスでデータを正規化することができます。
ハンバーガーショップで例えるなら、Requestクラスは注文を管理する「メニュー」になります。 ハンバーガーショップでは、日本語のメニューでも英語のメニューでも写真によってカウンタースタッフが注文の理解をしやすくなっています。
これを踏まえた上で、ハンバーガーショップで説明してみましょう。
メニューは、カウンタースタッフが理解しやすい注文を作るのが仕事です。
重要な点としては、RequestやControllerでビジネスロジックを書くようなら正規化が足りないということです。
処理が肥大化してしまうなら、可能な限り、別途画面を用意してデータを正規化するように調整しましょう。
Controller
Controllerは、Requestを受け取り、Serviceから完成されたModelを取得して、Viewと合わせるのが仕事です。
Controllerに仕事をさせないようにViewレンダリング時やRequestで可能な限りデータは正規化しましょう。
ハンバーガーショップならカウンタースタッフがこの役割にあたります。
カウンタースタッフは、注文を受け取り、ストックしている棚から完成されたハンバーガーを取得して、トレイと合わせるのが仕事です。
カウンタースタッフに英語や日本語を覚えさせなくてもよいようにメニューを可能な限りわかりやすくしましょう。
重要な点として、Controllerは完成されたModelを扱うということです。 今回の場合はデータの取得ですので、Modelオブジェクトの値をControllerで更新すると以降の処理で副作用が起き、後悔しか生まれません。コマンドとクエリの責務分離は大事です。やめましょう。
ハンバーガーショップで例えるなら、カウンタースタッフが完成されたハンバーガーの袋を開けてサンドイッチに変更し始めたら、違和感がありますよね?それと同じです。
Service
Serviceは、Controllerが欲しがっている完成されたModelを提供するのが仕事です。 効率をあげるためにServiceは、Modelをキャッシュする場合があります。 必要であれば、ルートModelに紐づくオブジェクトをEager Loadingしておきます。
ハンバーガーショップなら、ストック(キャッシュ)している棚が同じ役割をしています。 ハンバーガーショップで説明してみます。
棚は、カウンタースタッフが欲しがっている完成されたハンバーガーを提供するのが仕事です。 効率をあげるために棚は、ハンバーガーをストックしている場合があります。 必要であれば、ハンバーガーセットに紐づく商品をまとめて袋に入れておきます。
ここでも重要なのは、Serviceは完成されたモデルを扱うということです。 Controllerと同様にデータの取得ですので、Modelオブジェクトの値をServiceで更新すると以降の処理で副作用が起き、後悔しか生まれません。コマンドとクエリの責務分離は大事です。やめましょう。
棚に置いておいたら、完成されたチーズバーガーが突然ハンバーガーになることは現実世界で起きないのと同じです。
もし変化してしまったら誰がその責任を負うのでしょうか?それと同じです。
Repository
Repositoryは、Service(または、Controller)と永続化されたデータをやり取りするのが仕事です。
簡単に言えば、Factoryとのやり取りを抽象化することが役割となります。
Factoryの担当が、MySQL、Postgres、Mongo、APIに変化しても意識させません。
Laravelでいうと、Eloquentのcreateメソッドやgetメソッド、firstメソッドなどが該当します。
ハンバーガーショップでは、この役割を担うことが少ないと思いますが、例えるなら下記になります。
作られたハンバーガーを棚におく人は、棚(または、カウンタースタッフ)と冷蔵庫の材料から作られたハンバーガーをやり取りするのが仕事です。 簡単に言えば、ハンバーガーを作るスタッフとのやり取りを抽象化することが役割となります。 ハンバーガーを作るスタッフの担当が、アルバイト、超一流のシェフ、別の店舗からハンバーガーを持ってくることに変化しても意識させません。
ORMを使用していればあまり意識しないですが、大事な概念ですね。
Factoryの役割
Factoryは、Modelが欲しいと言われたら、DBから情報を集めて、Modelオブジェクトを作るだけが仕事です。 Factoryは、Repositoryのおかげで、別の代替手段に切り替えやすい存在となっています。
ハンバーガーショップであれば、ハンバーガーを作るスタッフが該当します。
ハンバーガーを作るスタッフは、ハンバーガーが欲しいと言われたら、冷蔵庫から材料を集めて、ハンバーガーを作るだけが仕事です。 ハンバーガーを作るスタッフは、ハンバーガーを棚におく人のおかげで、別の代替手段に切り替えやすい存在となっています。
Factoryを切り替えられることによって、多様性が得られていますね。
Model(Entity)
情報のかたまり、あるいはオブジェクトです。 私たちがWebサイトにアクセスしたときには、いつもModelという情報を取得しています。
Modelの役割をハンバーガーショップでごり押しで説明してみます。
おいしさのかたまり、あるいはハンバーガーです。 私たちがハンバーガ-ショップに行った時には、いつもハンバーガーという商品を食べています。
ハンバーガーであれば、カロリーという情報を持ち、味、ソース、パンという構造も持っています。 この場合、情報はValue Object、構造はRelationと言えるでしょう。
もし欲しいもの(Model)が手に入らなかったら、がっかりしませんか?Modelはアクションに対しての本質と言えますね。
View
Viewは、Modelを展開するだけが役割です。 また、Viewを使わずにJson形式で出力する場合もあります。
Viewの役割をハンバーガーショップで説明してみます。
トレイは、ハンバーガーを盛り付けるだけが役割です。 また、トレイを使わずに紙袋で渡す場合もあります。
ここで大事なのは、トレイの上でハンバーガーを展開することはあっても、ハンバーガーを材料にして加工することはないので注意しましょう。 調理道具のないトレイで火を起こして料理をすると大火傷しますよ?
プログラムも同じように、Viewの上ではModelを加工せずに展開するだけにしましょう。
ハンバーガーショップを例にしてのデータの取得の説明はこれで終わりです。
データの取得についてのみの説明だったため、データの更新時について、少しだけ補足します。
補足
弊社では、データの更新もServiceを利用していますので、基本的には大きく変わりませんがデータの更新の場合、注意が必要になります。 それは、データの更新の場合、Modelオブジェクトの値を更新することが前提なので、副作用が起きるということです。 そのため、以降の処理で大きく影響を受ける可能性がありますので、状態を変化させたModelの再利用はやめましょう。 状態が変化したModelは、一度破棄をして、再度データベースへ問い合わせるのがベストです。
RESTの概念で言えば、StoreやUpdateした後に、Showにリダイレクトするのと同じです。
MySQLであれば、InsertやUpdateした後に、データが取得できないのと同じです。
Reactで言えば、Stateの状態を更新した場合、Viewを再度レンダリングするのと同じです。
テキストファイルで言えば、更新して閉じた後に、再度開いて見直しするのと同じです。
普段から意識せずとも理解していることが多いのではないでしょうか。これらを意識して開発をするときれいに実装できると思います。
簡単にまとめるのであれば、下記のように覚えておくと良いでしょう。
- データを更新する処理では、状態を変更したModelの再利用は絶対ダメ。
- データを取得する処理では、Modelの状態変更は絶対ダメ。
まとめ
長くなりましたが、以上がオールアバウトでのLaravelとの付き合い方になります。
ここで記載した内容は、Laravelだけでなく、一般的なWebフレームワークに置き換えることもできます。 Webフレームワークを使用しての実装や設計に悩んでいる方の参考になれば幸いです。
最後になりますが、私はハンバーガーショップでは働いたことがありません。ご注意ください。
オールアバウトでは、一緒にハンバーガーを作ってくれるサービスを開発をしていくエンジニアを募集しています。
ご応募お待ちしております。