背景
昨年公開したこちらの記事「Laravel でドメイン駆動設計(DDD)を実践し、Eloquent Model 依存の設計から脱却する」の続編です。
弊社で開発している「オンライン家庭教師マナリンク」の実装に DDD のアプローチを用いています。導入して 1 年が経過したので、いろいろと所感を述べていきます。
目次
- どうして DDD を導入したか
- DDD で達成できたこと
- まだ難しいと思うこと
- 結局モデリングが一番難しい
どうして DDD を導入したのか?
導入した当初の動機は Laravel で Eloquent Model 中心に実装するアーキテクチャに嫌気が差したからです。
Eloquent Model 中心に実装するとあちこちにドメイン知識が散らばって、実装した自分ですらよくわからない、という状態によく陥りました。弊社はベンチャー企業で、トラフィックの多さを設計で考慮することはあまり無いのですが、一方で機能追加が盛んなため、より問題が顕著でした。
DDD を実践した期間
導入して 1 年、と言いましたが、今年実装したものを思い返すと、
- 決済機能(決済処理、決済の対象となる指導コースの CRUD、各種社内管理画面等)に 4 ヶ月
- 先生 ↔ 生徒のコミュニケーションツール開発に 3 ヶ月(Nuxt↔Firebase↔React Native)
- 先生検索機能に 2 ヶ月
- ドメイン変更 2 ヶ月(事業のピボット)
- その他 1 ヶ月
という感じで、DDD をやったのは決済周りなので、4 ヶ月程度しかやってないですね。
※フロントエンドは簡易的な Repository パターンとか Hooks を導入する程度で、DDD はやっていないです。
実装した PHP のクラス数は雑に見て 200〜300 くらい、テストコードは 100 くらいでした(普段結合テストしか書いてないことがバレるw)。
DDD で達成できたこと
DDD を導入することで達成できたことを、【アーキテクチャ】と【ドメイン】の観点でまとめます。
【アーキテクチャ】Eloquent Model からの脱却
まず、先の記事のタイトルにもあった”Eloquent Model からの脱却”ですが、一言でいうと達成できました。
Eloquent Model の各クラスには以下のような変化がもたらされました。
- 個々の機能でのみ使われるような実装がなくなった
- 結果として、リレーションの定義や、ごくわずかな Scope が残りました
そして、そのために以下の方針で実装を行ってきました。
- Repository でのみ Eloquent Model を活用する
- Repository では取得したデータは原則として Entity に詰め替えて返す
ものすごくシンプルな Repository Interface の例を貼っておきます。Repository の戻り値が Entity になっているのがポイントです。
<?php
namespace Packages\XXXXX\Domain\Repository;
use Packages\XXXXX\Domain\Entity\GeneralUser;
interface GeneralUserRepositoryInterface
{
public function find(int $id): ?GeneralUser;
}
【ドメイン】Entity にモデルの振る舞いを表現させる
機能名を表す namespace (ここでは”XXXXX”) の配下に 一般ユーザーを Entity とした"GeneralUser "というクラスを用意し、メソッドとして一般ユーザーがサービス上で行う振る舞いを実装していきます。
以下のようなイメージです。
namespace Packages\XXXXX\Domain\Entity;
interface GeneralUserEntityInterface
{
/**
* ユーザーは問い合わせと、支払いと、問い合わせのキャンセルができることがInterfaceだけ見ると読み取れる
*/
public function inquire(Teacher $teacher): Inquiry;
public function makePayment(Inquiry $inquiry);
public function cancelInquiry(Inquiry $inquiry);
}
※Entity の Interface までわざわざ作成することはさほど多くないですが、たまに作ります
以上のように、機能ごとに異なるふるまいを持っている Entity を切っていくことで Eloquent Model の責務を削るというアーキテクチャ上のメリットはもちろん、サービス上で誰(アクター)が何をできるのかが機能ごとに明示されるようになり、ドメイン知識が明確になるというメリットを得ました。
まだ難しいと思うこと
続いて、DDD を実践する上で難しいと思うことを書きます。
一覧系で Entity を使うと問題が生じる
Entity を中心に設計していると、一覧系のページが増えるに従って、どんどん Entity が肥大化していってしまう問題が浮上しました。
どうして一覧系で Entity を使うと肥大化するのか?
一覧画面で表示したいデータと、Entity に持たせるべきデータが必ずしも一致しないからです。
例えば弊社ですと、オンライン家庭教師の一覧画面で、各先生にその先生が用意しているオンライン指導のプラン(指導コースと呼んでいます)を 最大 3 件付帯して表示する、という要件がありました。
ここで、先生に指導コースを付帯して表示するためには、Teacher Entity に $courses といった命名のプロパティを持たせることで、表示系でも使えます。
しかし、この方法には「画面ごとに指導コースの取得ロジックや件数が違っていると、表示最適化のために Entity がどんどん Fat になっていく」という懸念があります。この画面では3件、あの画面では5件、この画面ではこの科目の指導コースに絞って最大3件、といった感じで条件が変わってくると Entity が肥大化します。
Teacher Entity にgetCoursesForHogeHogeListPage()
といったメソッドが次々増えていくのは本来ドメインモデルがやるべき責務なのか?という悩みがあります。言ってしまえば SEO 対策のために作るページも多いので、そこに Entity を使っているとどんどん表示ロジックが増えていきます。
対処方針
思いついた実装方針を 3 つ挙げてみました。私は(大半のケースにおいて)一番最初の方針を採択しています。
- 取得系の処理は、Entity にデータを詰め込まずに、専用の DTO クラス(API Resource のような責務)を使う
- 検索系の処理は Repository から返す値をそもそも DTO のクラス型にする
- API のパスから DTO まで一気通貫で分かりやすく namespace 等を管理できたり、OpenAPI の Model と上手に連携し、ViewModel のような概念で扱えるともっと良いのかもしれない
- 諦めて取得系の処理は Eloquent Model をそのまま取り回す
- 表示のための Attribute が Eloquent Model に生えまくって本末転倒感がある
- DDD をやるほどでもない機能や事業領域にはこちらを採用しています。正直言うと上記で例示した先生の一覧ページでは単に Eloquent Model の Collection を API Resource に入れてまるっと返しています
- Read はデータソースごと切り替えられるようにして CQRS にガッツリ取り組む
- 弊社はそんな事業規模じゃない(≒DB のパフォーマンスがボトルネックになっていない)
- とはいえ、事業規模が小さいなら DDD やらなくていいとか、適当に実装していいかというと別の問題。ちょうどいいバランスを模索していきたい
結局モデリングが一番難しい
一周回ってここ最近は DDD で一番難しいのは結局モデリングだなと思っています。
昨年初めて Laravel×DDD に取り組み始めた頃は、PHP でどうやって Entity を表現するか?ValueObject を表現するか?Enum は?といった実装レイヤの悩み事が多かったです。まあ今も多いんですが。
しかし、そのあたりに慣れてくると、そもそもモデリングの時点でどれくらい精度高くドメインモデルが導き出せるか、また、実装しながらどれだけモデリングを磨き込んでいけるかが重要なのではと思えてきました。
モデリングとはなにか?
(※個人的な定義です)
要件定義の文章から、モデルとモデル同士の関連性を導き出すことです。
要件はいつも文章で表現されますが、文章というのは大変曖昧だし複雑です。
「生徒がオンライン家庭教師に料金を支払えるようにして」という要件の中に、いったいどれだけのモデルが潜んでいるでしょうか。
見ただけでは「生徒」「オンライン家庭教師」「料金」「支払う(という行為)」が浮かびますが、ここにさらに「支払えるか支払えないかの前提条件」とか「支払うことのできる料金の範囲」といったルール的な概念もあれば、オンライン家庭教師から見ると生徒に支払ってもらった金額は「売上」に該当するので、「売上」というそもそも要件に現れていないモデルも見えてきます。
このように、要件に出てくる登場人物、アクション、条件、アクションの結果生成されるデータといったモデルを見抜き、それらの関連性を見出すのがモデリングです。
モデリングが大事だと思う理由は?
要件の抜け漏れを発見できたり、要件の広がりに答えられる可能性を作れるからです。モデル同士の関連性を図に表していると、「あれ?ここって 1:1 のつもりで書いているけど 1:多にならん?」といった初歩的な見落としに気がついたり、稀にですが「この登場人物とこっちの登場人物は汎化の関係になってないか?」といった、最終的に Interface や抽象クラスに落とし込めるヒントを得たりすることがあります。
実践 DDD といった書籍も読みましたが、書籍は引き出しを与えてくれるだけで、実際にどの引き出しをいつ開くかは自分が決めるべき、といった感じです。
つまるところ、DDD のパターンを脳死で導入しないためにモデリングをしている気がします。
私はドメインサービスとかイベントはあまり使わないようにしています。知った当初は面白い概念だなと思いましたが、モデリングも実装もままならないうちにこれらに手を出すとカオスになりました。実装が落ち着いたり、サービスの検証を回していって要件がしっかり固まってきた頃に見返すと、ここはドメインサービスが必要そうだ、といったことがようやく見えてきた気がします。
どうやってモデリングしているか?
ユースケース図を書くか、クラス図を書いています。クラス図を plantUML で書くのがお気に入りなのですが、本当は紙とかホワイトボードをもっと使いこなすほうがいいのかもしれないです。なんだか plantUML 書くのに必死で発想に集中できない気がするんですよね。クラス図がモデリングとイコールかというと違う気もしますし・・・
@startuml テスト用
note top of samplePackage
ノートを書くことができる
end note
package samplePackage {
class InquireUser
InquireUser : id: UserId
InquireUser : +makePayment(Teacher, amount): PaymentHistory
class Teacher
Teacher : id: TeacherId
InquireUser "0..1" -- "*" Teacher
class PaymentHistory
PaymentHistory : teacherId
PaymentHistory : +getUserPaid()
PaymentHistory : +getUserId()
PaymentHistory : +getCreatedAt()
InquireUser "0..1" -- "*" PaymentHistory
}
@enduml
実際モデリングする時間はあるのか?
実際、モデリングだけで丸 1 日掛けてます!とかは無いです。長くても数時間で終わらせたら少しずつソースコードを書きます。まずは Entity や ValueObject から書いて、Repository の Interface だけ書いて、組み合わせて UseCase を作っていくわけですが、節目節目で不自然なところがないか振り返り、こうやったほうがより的確な表現なのではと思ったら更新していきます。
Repository は Interface だけ書くことで、データベース設計と脳味噌を働かせるタイミングを分けて、API も作らず UseCase までで留めて一旦書き上げることで、ついついフロントエンドを実装したい衝動を抑えたりしています。延々とモデリングばかりして動くものを作らないと事業としては意味がないので、開発速度はできるだけ変えずに、開発の進め方を改善している感じです。
その他所感とか言いたいこと
Enum 便利
Enum は以下の抽象クラスを使っています。便利です。
<?php
namespace Packages\Base\Domain\ValueObject;
use InvalidArgumentException;
use ReflectionObject;
/**
* @see https://speakerdeck.com/twada/php-conference-2016?slide=40
*/
abstract class Enum
{
private $scalar;
public function __construct($value)
{
$ref = new ReflectionObject($this);
$consts = $ref->getConstants();
if (! in_array($value, $consts, true)) {
throw new InvalidArgumentException("value [$value] is not defined.");
}
$this->scalar = $value;
}
final public static function __callStatic($label, $args)
{
$class = get_called_class();
$const = constant("$class::$label");
return new $class($const);
}
final public function value()
{
return $this->scalar;
}
final public function __toString()
{
return (string)$this->scalar;
}
}
ValueObject をもっと上手くなりたい
ValueObject のモデリングについては、先日以下のツイートを見かけまして、ValueObject にはまだまだ開拓の余地があるなと思いました。
ビジネスルールを表現する5点セット。
— 増田 亨. (@masuda220) December 5, 2020
値オブジェクト(金額、数量、日付、地点、...)
区分オブジェクト(商品区分、サービス区分、状態区分、...)
範囲オブジェクト(価格帯、期間、地域、...)
コレクションオブジェクト(順序・集合・写像)
表オブジェクト(価格表、判定表、...)
まだまだビジネスルールを UseCase に書いてしまっている余地がありそうです。範囲オブジェクト、区分オブジェクトあたりは結構 if 文で済ませてしまっているような気がします。
ルールを個別のオブジェクトとして明示することで、より仕様をソースコードで表現しやすくなるように見えます。モデリングの段階で、こういった条件や範囲を意識的にモデリングするとソースコードにも反映できやすそうです。
Interface は神
最近抽象クラスを使わずに Interface で疎結合に組むのが楽しいです。Repository だけではなく、外部アクセスも Interface にします。また、これはOOPの話になりますが、特定の Entity に特定の振る舞いを持たせるためにその振る舞いのみを持った Interface を Implements させる(つまり、複数の Interface を Implements した Entity を作ったり、Interface を Implements した抽象 Entity クラスを作り、その拡張として実 Entity を作成する)など、Interfaceを有効活用してEntityを組むのも割と好きです。
Interface の扱いは今後も考察していきたいです。
▼ 参考 この質疑応答がとてもいいです
https://softwareengineering.stackexchange.com/questions/398455/depend-on-ddd-entities-or-interfaces
Using Object-Oriented Programming techniques helps you define your domain model and describe the state of all entities, as well as the relations between different entities.
テストコードは神
PHPUnit でテストコードを書くようにしています。ほとんど API 単位の結合テストだけしか書いていないのにも関わらず、大変品質に貢献してくれています。GitHub Actions で自動テストを回すので、思わぬデグレをほとんど事前に防ぐことができます。
来年は「テストデータの整備」「単体テストにもチャレンジ」「フロントエンドでも jest×composition-api でテスタブルにする」あたりを標語に頑張っていこうと思います。
まとめ
ドメイン駆動設計を導入して 1 年間運用することで達成できたことと、今後の課題点を述べてきました。
アーキテクチャ面では Entity を使いこなすことで Eloquent Model への依存を実際に卒業できました。
ドメインをモデリングする部分はまだまだ上達の余地があると考えており、アーキテクチャや設計パターンへの理解を深めることと両軸で上達していくことが重要だと思っています。
告知
私が CTO を務めている「オンライン家庭教師マナリンク」では、エンジニアを募集しております。
マナリンクでは、オンライン家庭教師の先生方のために、サイト上で自身のプロフィールを魅力的に発信できるようにしたり、オンライン指導専用アプリをリリースするなど、次々にプロダクトを開発しています。日々新しい技術を勉強して、試す機会を探している方にはうってつけな環境です。
ベンチャー企業ですが、CTO/CEO ともにテストコードを使って品質保持することに理解はありますし、オンライン家庭教師という新しい働き方のドメインを作っていくという意味で、問題解決領域としても大変興味深いのではと思っています。
興味あれば 上記の Twitter に DM でご連絡をください!