ドメインガード関数で不変条件を一元管理する
複数のフィールドの組み合わせでパネルや状態を決定するUIで、ドラッグ&ドロップなどの操作を追加すると矛盾した状態が生まれやすい。パネル判定ロジックが複数箇所に散らばっていたのが原因だった。
解決策として、不変条件をドメインガード関数に集約し、パネル判定も単一の関数に一元化した。
構造:
domain/
item-input.ts ← ドメインガード(不変条件のバリデーション)
item-panel-grouping.ts ← パネル判定の唯一の情報源
ui/
multi-panel-screen.tsx ← ガード関数を呼ぶだけ
ドメインガードの例:
// isArchived=true と activeGroupId!=null は共存できない
function validateItemInput(input: ItemInput) {
if (input.isArchived && input.activeGroupId != null) {
throw new Error("Archived items cannot belong to an active group");
}
}
パネル判定の一元化:
// グルーピングと同じロジックで単一アイテムのパネルも判定する
function determinePanelForItem(item: Item): PanelType {
return groupItemsByPanel([item])[0].panel;
}
// DnDハンドラは source × target のマトリクスで明示的に許可
function handleDragEnd(source: PanelType, target: PanelType) {
const allowed = { Backlog: ["Archive"], Archive: ["Backlog"] };
if (!allowed[source]?.includes(target)) return; // 無効な遷移は無視
// ...
}
なぜこうしたか:
- UI側にパネル判定の逆ロジックを持つと、グルーピング関数との間で不整合が起きる。判定を1箇所にすることで「UIが見ているパネル」と「ドメインが認識するパネル」のズレがなくなる
- ドメインガードを永続化層の手前に置くと、APIからもUIからも同じ不変条件が強制される
- DnDの遷移をマトリクスで管理すると、許可されない遷移を明示的にドキュメントできる