はじめに
担当しているプロダクトには 「広告を入稿する」機能 があり、これは社内のユーザ部門の業務効率化が主な目的です。
今までのフローとして、お客様に「広告設定確認書」という書類を提出して、
その「広告設定確認書内の広告設定」を元に「当該の広告媒体へ入稿を行う」業務フローがありました。
このフローをシステム化することで、業務効率を上げていました。 システムの内部の処理は、 「独自ドメインモデル(広告設定確認書内の広告設定)」を定義し、それを「外部ドメインモデル(媒体API・SDK)」に変換して入稿 API を叩く、という二段構えのフローを採用 していました。
このプロダクトを運用していくうちに、運用においても開発においても課題が見えてきました。
プロダクトの課題
- 「独自ドメインモデル(広告設定確認書内の広告設定)」 を 「外部ドメインモデル (媒体API・SDK)」 に変換する際に、スキーマの不一致によってデータの互換性が取れず、API エラーが起きやすい
- 「独自ドメインモデル」、「外部ドメインモデル」と理解するモデルが2つあり、認知負荷が高い
- データ互換性維持のための差分対応が属人化し、特定の人しか直しづらい状態になっている
まとめると「認知負荷が高い」と「データ互換性維持が困難」、「独自ドメインモデルに属人性がある」という課題 がありました。
また、広告媒体 API はアップデートされる機会が多く、このような問題に陥るケースが多くありました。
そのことから、現状の設計を根本から見直して、課題を解決する方法を考えました。
実装した設計
コンセプト
「外部ドメインモデル駆動」 とする設計です。
外部ドメインモデルの変化をシステムがしっかりと影響を受けて、システムが変化する ことを狙っています。
つまり外部のドメインモデルに主体性があります。
API 更新頻度が高いため、ACL 層を維持して独自ドメインモデルを管理するよりも、外部ドメインモデルに直接追従する方が、全体のコストを下げて安定運用できると判断して、 ACL 層を外す判断をしました。
これは一般的な設計の方針から外れる大胆な判断ですが、「API に追従することが最優先の領域」 では合理的だと考えました。
その他のコンセプトを箇条書きすると以下となります。
- 特定のオブジェクトのスキーマ(今回で言うと入稿 API を実行する際に必要なオブジェクト)を外部ドメインモデルに寄せている
- SDK のスキーマをミラー(写像)した Interface を定義している
- その Interface を持つオブジェクトを各層に実装する
- Infrastructure 層
- API や SDK とやり取りする層
- Presentation 層
- フロントエンドの関心を持つ層
- バリデーションの定義を持つ
- API スキーマとして表に出す層
- フロントエンドの関心を持つ層
- Entity 層
- DB に保存する関心を持つ層
- Infrastructure 層
- いずれの層は同じインターフェース(スキーマ)が実装されているので「オブジェクト to オブジェクト」のような変換が容易。
- フロントエンドで来たものを Entity 層に変換したり、Infrastructure 層に変換して API の実行ができたりと、レイヤーの意味を考えながら実装ができる
- その Interface を持つオブジェクトを各層に実装する
- 本来あるはずの Anti-Corruption Layer (ACL) 層 を外して、外部ドメインモデルのスキーマを自システムに影響を反映させる
また「SDK のスキーマをミラー(写像)した Interface」は以下のように「getter」を定義して、 スキーマの互換性を表現しています。
interface CampaignInterface {
public function getName(): ?string;
// SDK スキーマと同じフィールド
}
気をつけていること
- SDK のオブジェクトを「我々のコード」に注入せず、SDK のスキーマをミラー(写像)した Interface に依存している
- SDK のオブジェクトは基本的に使いづらかったり、実装が汚れがちであるため、Infrastructure 層だけ SDK のオブジェクトを扱うようにして、実装の汚れを閉じ込めている
- いわば「依存関係逆転の原則(DIP)」を取り入れて、「SDK -> Interface -> Domain」の関係性を作っています。
この設計を取り入れるメリット
- API 更新への追従が容易になる
- API の更新内容を見て、変更があれば「我々のシステムの外部ドメインモデルを変更する」、という一対一の関係ができる
- 属人化を排除できる
- モデルの説明を媒体ドキュメントに委譲できる。
- データの互換性の維持が容易になる
- 抽象層(独自ドメインモデル)を介さず、媒体スキーマに準拠しているため、適合性が高くできる
この設計を取り入れるデメリット
- 「外部ドメインモデル(媒体API・SDK)」の理解が必要
- ただし、これは広告運用プロダクトを扱う以上、避けられない前提知識でもある
- 「外部ドメインモデル(媒体API・SDK)」が廃止した時、プロダクトの改修範囲が大きくなる
- 外部ドメインモデルをそのままコピーするのでファイル数が膨大になる
- SDK 側の仕様変更に強く依存するため「ドメイン固有の最適化」がやりにくい
- 例:「API のマイクロバジェット値(マイクロ単位の通貨値)」をシステムでは「円」として保存しておきたいなど
- 元々存在した「独自ドメインモデル(広告設定確認書内の広告設定)」を使わなくなったため、ユーザ部門のコントロール性が下がった。(ただし、今回の設計変更は、ユーザ部門の負うこととなるこの「デメリット」を許容してもらうことをまず合意したうえでとりかかった)
- ユビキタス言語もロストしているため、業務の影響は多少なりとも出ている
- 正規化したと考えても良いが、業務をシステム都合で変えてしまっているのは変わりがない
まとめ
抽象化は一見便利ですが、常に媒体 API に追従しなければならないシステムにおいては、むしろ負債 になりがちです。
一方で、抽象化することで 1 つのモデルで他媒体の関心を共通化できるメリットもあるので、
どの選択をしてもリスクとリターンがあると、総括して感じました。
今回の設計は独自のドメインモデルを捨てて外部ドメインモデルに従う、いわば「逆DDD」ですが、
認知負荷の低減・属人化の排除・最新 API への追従を考えると、やはりメリットが多いと感じます。
そのことから「むしろ API にしっかりと追従する必要があるシステムには 逆DDD が良い」という提案をしました。
実際に、この設計を今のプロダクトに適応している最中ですが、 API の追従がしやすかったり、スキーマに互換性があるため、API を叩きやすかったりと効果的だと感じています。
とはいえ、知れ渡っている理論を逆行する設計なので、心理的にも思い切りが必要です。
そのため 「逆DDD」を採用する時は、しっかりと「そのシステムで何をしたいのか?」を明確化して判断する のをオススメします。
所感
自分でモデリングする機会が減りました。
面白いところだと思いますが、少し残念です。
ひたすら外部ドメインモデルのリファレンスを読んで、
インターフェースやクラスを書いているので、作業感がとてもあります。
(かなり膨大なので疲れます)
それでもシステムは安定してきているので、作っているシステムにとって良い設計だろうと判断しています。