shwldshwld7 days ago

ドメインガード関数で不変条件を一元管理する

複数のフィールドの組み合わせでパネルや状態を決定する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の遷移をマトリクスで管理すると、許可されない遷移を明示的にドキュメントできる