はじめに
こんにちは、バックエンドエンジニアの@zawaです。
私は入社以来、1年ほどショップオリジナルの「メンバーシップ」(会員制度)を開設できる「メンバーシップApp」の開発に携わってきました。
少し前になりますが、2024年2月末にメンバーシップAppの特典交換機能をリリースしました。
リリース内容の詳細はぜひこちらをご覧ください!
メンバーシップAppは、モジュラーモノリスのアーキテクチャ上に構築しており、モジュール内部ではドメイン駆動設計(以下、DDD)を採用しています。
先日公開された動画の中でも紹介していますので、ご興味がある方は是非ご覧ください。
【前編】クリーンアーキテクチャの柔軟性を生かしたメンバーシップAppの開発の道筋 - YouTube
【後編】クリーンアーキテクチャの柔軟性を生かしたメンバーシップAppの開発の道筋 - YouTube
本記事では、初めてDDDを採用したチームが直面した課題と、またそれらをどのように克服していったのかをお伝えしたいと思います。何か参考になることがあれば幸いです!
プロジェクトの概要
メンバーシップAppの開発は、次の3段階でリリースし、1st~3rdまでの総期間は1.5年ほどかかりました。
- 1st:メンバーシップを開設できる機能
- 2nd:ショップオリジナルのポイントを貯められる機能
- 3rd:購入時に貯まるオリジナルポイントと、ショップで設定した特典を交換できる機能
領域を分けてコンフリクトしないようにしつつ、2チーム体制で開発を行っていました。
エンジニアだけでも15人前後は関わっており、かなり多くの人が関わっていたプロジェクトでした。
私は1stの途中からチームに参加し、主にショップオーナーさん向けの機能開発を担当してきました。
1st、2ndで得た学び
初めのうちは、チームでの輪読会や組織内の有識者への相談を通じて、徐々にDDDに関する理解を深めていきました。 2ndのリリース後の振り返りの中で、どうすればさらに改善できるかについて、具体的な振り返りを行いました。
データモデリングとドメインモデリングの誤解とその影響
まず、1st、2ndでは、データモデリングとドメインモデリングのそれぞれの目的を正しく理解できていなかったね、という話題が出ました。
ドメインモデリングではドメイン領域に焦点を当てて会話し、ドメイン領域への理解を深めることが重要ですが、私たちのチームはデータの永続化や具体的なデータ構造に関する議論に偏っていました。
事前にデータモデリングの勉強会を行い、テーブル構造を事前に検討した影響もあってか、エンティティや値オブジェクトを特定した後も、「このエンティティの永続化が必要か」「どのタイミングで永続化すべきか」「どのようなデータ構造が適切か」という永続化の観点が議論の中心になってしまい、ドメインの振る舞いやルールについて十分な検討を行う前にドメインモデルの実装に進んでしまいました。後になってから、永続化とドメインモデルは別々に考えるべきだった、ということに気づきました。
これによって、次のような実装になってしまい、オブジェクトの整合性も保ちづらく、保守性も低い上に理解しづらい実装になってしまいました…😢
- ドメインルールや振る舞いについて深く考えることができなかった結果、本来ドメインモデルにあるべき実装がアプリケーション層やドメインサービスに漏れてしまうことがあった。
- 集約について議論していなかった結果、テーブルとRepositoryが1対1になっていたり、ロジックの置き場所に困ることがあった。
例として、メンバーシップの編集をする際のメイン画像の登録/削除ロジックは、アプリケーション層に次のように実装していました。(⚠️サンプルコードなので実際の実装とは大きく異なります)
当時は手探りの中、スピード感を持って実装していたので致し方ないのですが、本来ドメインモデル振る舞いの中で実行されるべきロジックが、アプリケーション層に漏れ出てしまったことで、テストのしやすさや保守性が損なわれていました。
/** * リクエストに画像URLが存在していないとき * ・ 既に画像が保存されている場合は削除する * ・ 画像が保存されてされていない場合、何もしない * * リクエストに画像URLが存在しているとき * ・ 既に保存されている画像があるとき * ・ 同じ場合、何もしない * ・ 異なる場合、古い画像を削除して新しい画像を保存する * ・ 画像が保存されていないとき、新しい画像を保存する */ // リクエストの画像URLを取得 $requestImageUrl = $request->getImageUrl(); // データストアから、メンバーシップを取得 $membership = $this->membershipRepository->find($membershipId); // データストアから、登録済みのメイン画像URLを取得 $savedImageUrl = $this->mainImageRepository->find($membership->getId()); if (is_null($requestImageUrl)) { if (!is_null($savedImageUrl)) { // 画像が保存されている場合、削除 $this->s3ImageRepository->delete($savedImageUrl); } // 画像が保存されていない場合、何もしない } else { if (!is_null($savedImageUrl)) { if (!$requestImageUrl->equals($savedImageUrl)) { // 保存されている画像が異なる場合、古い画像を削除して新しい画像を保存 $this->s3ImageRepository->delete($savedImageUrl); $this->s3ImageRepository->save($requestImageUrl); $this->mainImageRepository->save($requestImageUrl); } // 同じ場合、何もしない } else { // 画像が保存されていない場合、新しい画像を保存 $this->s3ImageRepository->save($requestImageUrl); $this->mainImageRepository->save($requestImageUrl); } } // メンバーシップを保存 $this->membershipRepository->save($membership);
モデリングへのフィードバック
実装したドメインモデルについて、使いづらさや保守性の観点での違和感を感じつつも、修正することができなかったことに関して、「途中で立ち止まる時間が欲しかった」という話題も出ました。
ドメインモデルを実装し、実際にユースケースから使ってみて、その結果をモデリングにフィードバックするサイクル回すことが重要なのは理解しながらも、実務の中で再ドメインモデリングをしようと言い出すことは一定のハードルがあることが振り返りの中でわかりました。
ドメインモデルは色々なユースケースから使われるので、再モデリングをする場合、認識を合わせたり、改修するコストが高くなるのではと感じてしまい、結果として修正するアクションができなかったのではないかと思われます。
3rdではどうなったか
2ndリリース後の全体振り返りでうまくいかなかった部分について振り返ることができ、3rdでは改善することができました!
3rdではデータモデリングとドメインモデリングを分けて考え、最初からドメイン領域に焦点を当てた設計をする動きができました。ユースケースをもとに、エンティティや値オブジェクトを探し出す作業、そしてドメインルールや振る舞い、集約についても深く考えることができたと思います。
これまでは、ドメインモデリング図はホワイトボードツールで作成をしていましたが、作成したモデリング図は明確な管理方法が決まっていたわけではなく、コードに落とし込んだ後は使い捨てるようなケースもありました。そのため、議論に参加していないと、どのような理由でどう変更されたかがとても追いづらい状況でした。
再モデリングを気軽にやりやすくするために、3rdではmermaidで書いてGit管理するようにしました。Git管理することで、変更履歴を追いやすくなり、大きな変更でなければ共有はPRで済むようになり、ドメインモデルの修正作業が以前よりも気軽に行えるようになりました。
メンバーシップ特典登録では、特典画像を登録することができます。画像登録は1stで実装したメンバーシップのメイン画像のドメインモデルを流用できれば良かったのですが、先ほどの例のとおり、ロジックがアプリケーション層に漏れ出しており、流用するのが難しい状態でした。そこで、再モデリングをし直して、リファクタをしました。
メンバーシップのメイン画像はメンバーシップと同じライフサイクルをもつため、メンバーシップ集約ルートとし、メイン画像はその集約の中に含めることにしました。
これにより、メンバーシップの編集ロジック内に、画像の登録/削除のロジックを閉じ込めることができ、アプリケーション層がとてもシンプルになりました🎉
// リクエストの画像ファイル名を取得 $requestImageFileName = $request->getImageFileName(); // データストアから、メンバーシップ集約を取得 $membership = $this->membershipRepository->find($membershipId); // メンバーシップ集約が持つ、画像ファイル情報を取得 $savedImageFileName = $membership->getMainImageFileName(); $membership->edit( $request->getName(), $request->getDescription(), $requestImageFileName, ); if (!$membership->isSameMainImageFileName($savedImageFileName)) { $this->s3ImageRepository->save($shopId, $savedImageFileName, $requestImageFileName); } // メンバーシップ集約を保存 $this->membershipRepository->save($membership);
よいモデリングができると、複雑さなロジックをドメインモデルに閉じ込めることができるので、アプリケーション層では振る舞いを呼び出せばよく、とてもシンプルになる上に、責任が明確なので、とてもテストがしやすくなりました。試行錯誤してよいモデルができてからは実装速度が上がっていったように感じます!
一方で正解のないドメインモデリングにこだわりすぎてしまい、時間をかけ過ぎてしまっているかも?という感覚もありました。どこまで作り込むべきなのか、この辺りはとても塩梅が難しいところだなと感じました。
最後に
私自身、これまでの経験で、設計についてここまで深く考えたことはなかったのですが、このプロジェクトを通じて、チーム全体でDDDやOOPに対する理解が深まり、保守性の高いシステムの開発ができたのではないかと思います。
難しい挑戦ではありましたが、モチベーションの高いメンバーと一緒に理解を深めながら開発ができたことがとても良い経験となりました! また、スクラム開発を採用し、振り返りを重視したことで、1.5年にわたる長期プロジェクトの中でも、1st、2nd、3rdと各段階で以前の欠点を改善しながら前に進むことができたと思っています。
最後になりますが、BASEでは一緒に働くエンジニアを積極採用中です!
今回紹介したようなモジュラモノリスやドメイン駆動設計に少しでも興味を持っていただける方がいたら、ぜひご連絡ください!