依存性逆転の原則(DIP)はコードの設計原則として有名ですが、データの持ち方にもまったく同じ構造の問題が現れるという話です。
レビューを独立したドメインとして作った
レビュー機能を実装することになり、独立したドメインとして設計しました。注文レビューだけでなく、店舗レビューなど今後レビュー対象が増えることを見越してのことです。
case class Review(
id: ReviewId,
body: String,
rating: Int,
)シンプルで、何にも依存していません。
「注文の〇〇もレビューに表示したいんだけど」
ある日こう言われました。「レビュー一覧に、レビュー対象の名前も出したい」。
注文なら商品名、店舗なら店舗名——「レビュー対象の名前」という抽象で乗り切れます。revieweeName を足して解決しました。
case class Review(
id: ReviewId,
body: String,
rating: Int,
revieweeName: String, // レビュー対象の名前。抽象化でなんとかなった
)ところが次に来たのは「商品の価格も載せたいな」「画像も欲しいな」。
こうなるともう抽象化は無理で、レビューに種別を持たせて全プロパティを受け入れる根性が必要になります。私にはない。
なぜまずいのか
これには 2 つの問題があります。
バケツリレーとマイグレーション。注文の属性をレビューに表示したいとなるたびに、レビュー側へのデータ追加だけでなく、過去のレビュー分のデータマイグレーションも必要になります。属性が増えるたびにこのコストを払い続けることになる。
拡張性の喪失。レビューに itemPrice や itemImageUrl を持たせた時点で、レビューは注文専用になります。店舗レビューを追加したいのに、注文固有のプロパティが邪魔をする。無限に増える属性に対応し続けるのは、事業的にも時間の使い方としてイマイチですよね。
起きていることの正体は、レビューが注文に依存しているということです。コードの DIP は知っていても、データの持ち方で同じ罠にはまるんですよね。
解決策: データの依存方向を逆転させる
レビューに注文の属性を持たせるのではなく、注文側がレビューへの参照を持つようにします。
// レビューは自分のことだけ知っている
case class Review(
id: ReviewId,
body: String,
rating: Int,
)
// 注文がレビューへの参照を持つ
case class Order(
id: OrderId,
items: List[OrderItem],
reviewId: Option[ReviewId],
)これで API も自然な形になります。
GET /orders/111
{
"id": 111,
"items": [...],
"review": { "body": "良かった", "rating": 5 }
}注文経由でレビューを取得すれば、注文の属性は注文が持っているのでレビュー側にバケツリレーする必要がありません。注文に新しい属性が追加されても、レビュー側のスキーマは一切変わらない。マイグレーションも不要です。
注文の条件でレビューを検索したければ、注文を起点にすればいい。
GET /orders/reviews?itemId=222
[
{ "orderId": 111, "itemName": "ワイヤレスイヤホン", "itemPrice": 3980, "review": { ... } }
]/reviews も存在はしますが、こちらは注文の条件では検索できません。注文の属性で絞りたければ /orders/reviews を使う——データの所在と検索の責務が一致します。
DIP というとインターフェースを介すイメージがあるかもしれませんが、インターフェースは手段であって目的ではないです。DIP が本当に求めているのは依存の主導権をどちらが持つかということで、データでもそれは同じです。注文とレビューの関連自体はなくならないけど、どちらがその関連を管理するかが変わっています。
店舗レビューの追加
店舗レビューを追加するときも同じ構造で対応できます。
case class Shop(
id: ShopId,
name: String,
reviewId: Option[ReviewId],
)GET /shops/reviews?area=shibuya
[
{ "shopId": 1, "shopName": "カフェ○○", "review": { ... } }
]レビュー自体には何も手を加えていません。レビューを独立したドメインとして保ったまま、レビュー対象が増えても変更は局所化されます。
まとめ
- 依存性逆転の原則はコードだけでなく、データの持ち方にも現れる
- エンティティ A の属性をエンティティ B に持たせると、B は A に依存する
- 依存を逆転させる = A 側が B への参照を持ち、依存の主導権を移す
- 汎用ドメインを特定用途に依存させると、属性追加のたびにマイグレーションが発生し、他の用途への拡張も困難になる
- データの所在と検索の責務を一致させると、API 設計も自然になる