UiState にドメインモデルをどこまで持たせるべきか考えた
Jetpack Compose で画面実装をしていると、よく悩むのが、
- UiState にどこまでドメインモデルを持たせるべきか
- UI専用モデルをどこまで作るべきか
という問題です。
最近、自分は Id の扱いについて考える機会がありました。
きっかけ
たとえば、以下のような UiState があるとします。
data class UiState(
val id: Id = Id.Unassigned,
val name: String = "",
) {
sealed interface Id {
data object Unassigned : Id
data class Assigned(val value: Int) : Id
}
}
一方で、ドメインモデル側にも、ほぼ同じ定義があります。
sealed interface PaymentId {
data object Unassigned : PaymentId
data class Assigned(val value: Int) : PaymentId
}
さらに、今後ほかの画面でも、
PaymentEditUiState.Id
PaymentDetailUiState.Id
PaymentListItemUiState.Id
のような、似た ID 型が増えていく可能性がありました。
そこで、
「これ、UiState ごとに定義する必要ある?」
という疑問が出てきました。
結論
今回のケースでは、
ドメイン側の ID 型をそのまま UiState で使う
方が自然だと感じました。
つまり、
data class UiState(
val id: PaymentId = PaymentId.Unassigned,
val name: String = "",
)
のようにする方針です。
なぜそう考えたのか
ID は比較的「安定した概念」
今回の Id は、
- UI専用概念を持たない
- 画面仕様に依存しない
- ドメインでもUIでも意味が同じ
- 将来的に意味が変わりにくい
という特徴があります。
つまり、
「変化しにくい ValueObject」
として扱えると考えました。
本当に危険なのは「変化しやすいドメインモデル」
UiState がドメインモデルを持つこと自体が問題なのではなく、
「変化しやすいドメインモデルを UI が直接握ること」
が問題になりやすいです。
たとえば以下のような Entity をそのまま持つと危険です。
data class Payment(
val id: PaymentId,
val payer: User,
val histories: List<History>,
val auditLog: AuditLog,
val permissions: PermissionSet,
)
こうなると、
- DB構造変更
- API変更
- Aggregate変更
- ビジネスルール変更
などが UI に波及しやすくなります。
一方、ID は変化しにくい
しかし ID は、
- 「未採番」
- 「採番済み」
くらいしか状態がなく、意味も安定しています。
そのため、
sealed interface PaymentId
を UI とドメインで共有しても、影響範囲が爆発しにくいと考えました。
むしろ UI専用 ID を増やすデメリットもある
無理に UiState 専用 ID を作ると、
- 同じ意味の型が増える
- mapper が増える
- 微妙な仕様差が生まれる
- 認知負荷が上がる
という別の問題が発生します。
特に、
PaymentEditUiState.Id
PaymentDetailUiState.Id
PaymentListItemUiState.Id
のような型が増殖すると、
「全部ほぼ同じでは?」
という状態になりやすいです。
最近は「全部分離」が正義ではないと感じる
以前は、
UI とドメインは完全分離すべき
という考え方を強く持っていました。
しかし最近は、
- 安定した ValueObject は共有
- 変化しやすい Entity は分離
というバランス設計の方が、実装コストや保守性との釣り合いが良いと感じています。
たとえば、
data class UiState(
val paymentId: PaymentId,
val accountId: AccountId,
val title: String,
val items: List<PaymentItemUiModel>,
)
のように、
- ID
- enum
- Money
- Date
- ValueObject
などは共有し、
表示専用部分だけ UiModel 化する設計です。
まとめ
今回学んだことは、
「UiState にドメインモデルを持たせるかどうか」
ではなく、
「その型はどれくらい変化しやすいのか」
を考えることが大切だということです。
特に ID のような安定した ValueObject は、
- UI専用型を量産するより
- ドメイン型を共有した方が
- シンプルで読みやすくなる
ケースも多いと感じました。