Laravelで電子コミック書店をドメイン駆動開発(DDD)で実装した話

■ 概要

こんにちは。Applivマンガの開発の責任者の工藤です。

2018年1月ごろから電子コミック書店の開発をやって8月末に本リリースしました。(https://manga.app-liv.jp)

CGM、無料漫画コーナー、有料漫画ストアの3フェーズくらいに分けて開発してきた中で色々試してきたことがあり、

その中からいくつかやって良かったものをリリース後の振り返りも兼ねて投稿したいと思います。

書いてたら結構長くなってしまったので今回はDDDまわりのみとして他の内容は次以降に書きます(たぶん)。

DDDの基本的な話はググればいっぱい出てくるので飛ばしています。

なので対象読者としてはDDD系の資料はざっと適当に斜め読みして大体わかってるけどめんどくさそうだなと思ってる人で、そういう人が本稿で「あーなんかきれいにまとまりそうだな試してみたいな」と思っていただけるとこれ幸いです。

■ 実装方針としてのDDD導入

サービス全体を大きく分けてCGM、無料漫画コーナー、ビューワ、の3つのみまではフレームワークレベルの制約だけで十分でした。

(Laravelを使っていてMVCの分離だけで十分だった)

ここに有料漫画ストアが入った瞬間に単純に機能が増えること以上の複雑度の上昇が見えたためどうまとめるのが良いか色々調べてDDD系の資料に乗っかっていこうと決めました。

Laravelはこういう時に柔軟に構造改革がやりやすいようになってて良いです。

プロトタイプ~開発初期はバニラにMVCでサクサクと開発していき、事業がグロースしていってエンジニアもソースも増えていく時にstrictな設計を注入していくのがやりやすいです。

・開発スケジュール

2月15日:CGM(泣けるマンガランキング(https://manga.app-liv.jp/ranking/show/10)など特定の切り口で用意されたランキングトピックにユーザがマンガをコメント付きで投票できる簡単なCGM)

5月末:無料漫画コーナー(閲覧は無料だけど広告が出てくるやつ)

8月末:有料漫画ストア(普通に漫画を買って読めるやつ)

こんなスケジュールで開発したのですが、8月の最初の方にDDDの導入を決めて動き出しました。

・こんな感じでやった

いきなり全部に適用するのはきつかったのでモデルとして中心になる漫画データまわりから適用しました。

開発メンバーと話し合いながらwebに転がってるサンプルと比較検討しながら決めていきました。

分離や抽象化の境界だけ気をつけて細かいとこはあとでリファクタしようという感じで割とサクサクやったんですが後述するアプリケーション分割とPhpStormのリファクタリング機能も効いて副作用もなくスムーズに終えられたという印象です。

IDE嫌いな人けっこう多いと思うんですが、(僕はカーソル移動がもたつくので嫌いでした)PhpStormに関してはちょっと前からちゃんと触りだしたらとても便利で必須の道具になりました。

デフォルトで必要なものが全部入ってるのでカスタマイズの必要がないしめちゃめちゃ気が効くよくできた製品なのでまずトライアル30日試したことない人は是非試してほしいです。

https://www.jetbrains.com/phpstorm/

  1. パッケージ分割

Viewerや決済システムを外部のシステムを利用し提供されたSDKを利用するみたいな仕様があました。

これは交換可能にするためなるべく疎にアプリケーションの実装とは分けて配置しました。↓こんな感じ

Root(Laravel)

∟ app

∟ bootstrap

∟ config

∟ packages

— ∟ ApplivManga (弊電子コミックストアの実装)

— ∟ XXXXPayment(決済SDK)

— ∟ XXXXViewer(ビューワのソース)

2. アプリケーション層

CGM、無料漫画コーナー、有料漫画ストア、コインストアの4つにアプリケーションを分割しました。

Applicationレイヤにこの4つのアプリケーションを置いて、RoutingやRequest、Controller、UseCase、ViewComposer、Serviceなどアプリケーション固有の実装はここにまとめました。

アプリケーションによって必要なモデルの集約や特性が変わるのでそれぞれに最適化した実装や1つのアプリケーション固有の事情で必要な実装はこの層に閉じ込めます。

こうすることでアプリ固有のロジックをそのアプリ以外の世界と疎にして捨てやすく変更しやすい状態を保ちます。

後述のドメイン層にはモデルの性質や責任、関心が閉じ込められていて、一方でアプリケーション層はアプリケーションの機能的な部分です。

開発のスコープ的に前者は変更が少なく長い、後者は変更が多く短いといったような質の違うコードがそれぞれに集まります。

ドメイン層は慎重に開発すべきだけど細かい機能は決済まわりをのぞいてある程度手抜きできます。

(テストコードのカバレッジを細かいところまで100%にしてたら何人いても終わらないのでメリハリをつけるべきだと思います)

↓こんな感じ

∟packages

— ∟ ApplivManga

— — ∟ Application

— — — ∟ MuryoMangaCorner

— — — ∟ MangaStore

— — — ∟ CGM

— — — ∟ CoinStore

3. ドメイン層

「有料漫画」「無料漫画」「タグ」「巻データ」などのモデルはここにまとまっています。

アプリケーションの都合によってただ漫画データがほしいだけだったり、タグがついてほしかったり巻データがついてほしかったりするので、

必要なインスタンスや集約を返すためのFactoryやAggregaterなどの実装もここにおいています。

複数のテーブルを更新する必要があるような複雑なトランザクションのロジックなんかはこの層でつくられる複数のドメインの集合(集約ドメイン)で処理します。

例えば上記のアプリケーションのうちMangaStore(有料漫画書店)で有料漫画をユーザが購入した時に更新する必要があるデータはユーザの所持コイン・ポイントデータ、購入履歴データ、ユーザの行動ログデータ、などがあります。この購入処理はユーザが手動で行うものと、フォロー中タイトルの新刊自動購入機能による自動で行われるものがあります。

このようなトランザクションをメンテナブルに継続開発するためには「購入処理」という1つのpublicなメソッドのみをアプリケーション開発者に公開し、抽象化した購入処理の中に一連のトランザクションを隠蔽することでいちいちアプリ開発者がロギングやデータの整合性を気にしなくて済むようにします。

このようなモデル間の関連を表現するような実装などはこの層においてアプリレイヤのUseCaseやRequestからドメインモデルないし集約ドメインのルートドメインの公開メソッドから処理します。

(ロギングのストレージエンジンなどはインフラ層のリポジトリでDIできるようにします)

他にはドメインのメンバを表現するバリューオブジェクトの実装もここに入ります。

このような当該サービスにおけるモデルとその集約固有の関心、事情や関連をこの層に置いて、その他のものは別のレイヤの適切な場所に追い出すことで肝心な事をこの層で際立たせることができます。

↓こんな感じ

∟ Application

∟ Domain

— ∟ MangaTitle

— ∟ YuryoMangaTitle

— ∟ MuryoMangaTitle

— ∟ Factories

— ∟ Aggregaters

4. インフラ層

特に大きな懸念はなく通常のリポジトリ実装を置き、インターフェイスに従って書けば大きな問題はないと思います。

こうすることによってストレージエンジンをMySQLからRedisに取り替えたりMongoに切り替えるなど容易にできる作りになります。

ただエンジンに依存した実装(JOINゴリゴリとか)がここに入るのは微妙です。

アプリケーションの事情で複雑なデータの読みをやらないといけないときは、アプリケーション層で固有の事情を反映させたUseCaseとかServiceが専用のオブジェクトを返すみたいな実装になるようにします。

■ まとめ

以上、めちゃくちゃざっくり全体の設計イメージをレイヤベースで書き起こしてみました。

DDDの本質はドメイン層の洗練によって物事が現実世界で行うふるまいと実装のコンテキストを限りなく近づけることで現実世界の都合をきれいに実装に落とせる、反映することができる、というところだ思っています。

そのためのユビキタス言語であり、(ビジネスエキスパートとエンジニアと実装とで同じ言葉を喋ろう)レイヤアーキテクチャであり、オニオン、クリーン、ユースケース、リポジトリ、バリューオブジェクト、だったりするわけです。

なのでDDD導入の際にはこの目的と手段を取り違えないことが大事だなとやってみて思いました。

レイヤ化とかバリューオブジェクトはドメインないし集約ドメインの輪郭、責任、関心をより明らかにするための分離の手段であって、目的は常に「ドメインを、ビジネスロジックを、その相互関係性を」「明確に、正確に、できるだけ簡潔に」していくことです。

逆に言うとそれが成せるなら分離手段は選べます。分離手段自体も疎にして交換可能にしておくのが良いです。

・ある機能に仕様を追加したり削ったりする時に、その変更が起因するのが単純に『機能の内容変更からくるもの』なら個別のアプリケーション層の適切なクラスに変更が入り、適切な範囲に影響を与える。

・その変更が起因するのが『物事の性質の更新からくるもの』ならドメインモデルとその集約に変更が入り、アプリケーション全体に安全に影響を与える。

言うだけだと簡単そうですがこういった理想の状態をつくるのが本当に大変で、どんなにやっても完璧な状態というものは成らないもんですが、DDDの考え方はかなり完璧に近い状態をつくる、目指すのを助けてくれるなーと感じております。

■ おわりに

DDDは小さなアプリケーションではオーバーかなと考えていましたが、実際にやってみると規模に関わらず適用できるストレッチ可能な設計を敷けるものだと感じました。

全体的に(特にバリューオブジェクトをちゃんとやると顕著)分離のためにクラスを増やしにかかるのでとっつきにくい印象を持たれることが多いですが、がっつりやってみた結果の所感としては

・実装の解像度が上がって表現力が豊かになるから、

・コードを読んだ時に仕様がスッと頭に入ってきやすい

・結構直すとこ多そうだなと思った変更が+1行で済んだりだとか

色々と期待していた部分以上のところで都合の良いことが多々あり結果としてはやって良かったなぁという感じでした。

言語に寄らないソフトウェア設計・開発において普遍的に有益な考え方を与えてくれるものだと思うので、関心のある技術者には是非学んでみてほしいところだなぁと思います。

ただ、DDD系の資料いろいろあさって悟り開いて気づいたのはDDDというのはただの言葉じゃということです。陽炎のように近づいたら消えて無くなりました。

(構造的な部分の話はよく考えれば基本的なことで、すでにみんなもってる考え方を良い感じに集めてまとめたという感じだなと思った)

こうすればDDD、こうしたら天下無双、って感じではなくて、「適切な構造と開発の運用を元にビジネスとソースコードを上手に綺麗にスケールさせていけてる状態に今あるか?」

こういったプロセスを評価しようとした時にYESならそれで良いし、NOならこのDDDという1つの考え方が助けてくれるかもしれないという、そんなようなものでした。

次回はそんなDDD導入を大きく助けてくれたPhpStormというIDEの紹介と、asanaというプロジェクト管理ツールの紹介をしたいと思います。