Upgrade to Pro — share decks privately, control downloads, hide ads and more …

フロントエンドのモデルに境界を引く

Avatar for baby baby
October 12, 2026
390

 フロントエンドのモデルに境界を引く

フロントエンドカンファレンス関西2026スライド

Avatar for baby

baby

October 12, 2026

Transcript

  1. 開発環境 システム • • • BEから受け取った状態に応じて、UI の表⽰‧操作が変わる BEは状態を返し、表⽰や操作の可否 はFE側で判定することも多い 稼働中のシステムの保守開発

    組織体制 • • • • FE/BEでロールが分かれ、それぞれ 開発している エンジニアはチーム開発で複数⼈が 同時並⾏でシステムに変更を⼊れる システムによって業務を遂⾏する担 当者が別途存在する 業務に詳しい⼈とは⽇常的には話さ ないが、要件定義などの場で質問‧ 確認はする
  2. ⽇々の開発の課題感 • 仕様変更のコストが重い... ◦ ◦ ◦ ◦ ◦ 変更すべき箇所の洗い出しに時間がかかる 認識齟齬からの対応漏れ

    画⾯要件がビジネス要求に整合しているのかの確認が難しい BEとFEの実装が整合していない 誰がどの部分の仕様を決めているのかわかりにくい 判定が画⾯ごとに散らばっていて、どこに何があるか追いきれない。 その判定がデザインの都合なのか業務の決まりなのかわからない。 エンジニア内外のコミュニケーションのコストが⾼い。
  3. 3. モデルを実装する-APIレスポンス型API レスポンスは通信回数の省略や他システムとの共通化など、 ドメインモデルとは無関係の都合で構造が決まることも多々ある // APIレスポンス(通信の都合で決まる形) export type OrderDetailResponse =

    { id: string; status: | "PENDING_PICKING" | "PICKING" | "PACKED" | "SHIPPED" | "DELIVERED" | "CANCELLED"; // 注文の状態とは別に進む(キャンセル後も返金中がありうる) paymentStatus: | "AUTHORIZED" | "AUTHORIZATION_FAILED" | "CAPTURED" | "REFUNDING" | "REFUNDED"; shippingAddress: 略; items: 略; totalPrice: 略; };
  4. 3. モデルを実装する-フロントエンドの概念型へ変換APIレスポンス型を、必要な概念の型に変換する(腐敗防⽌層) ルールが使わない⽀払い状態‧お届け先‧商品‧⾦額は持ち込まない // 概念:注文の状態(ドメインモデルの仮説) export type OrderStatus = |

    "PENDING_PICKING" | "PICKING" | "PACKED" | "SHIPPED" | "DELIVERED" | "CANCELLED"; // 概念:注文 export type Order = { id: string; status: OrderStatus }; // 腐敗防止層:APIの形から、ルールが使う概念だけを取り出す export const toOrder = (r: OrderDetailResponse): Order => ({ id: r.id, // APIのenumが増えたら、ここでコンパイルエラーになる // 状態の分け方がAPIと食い違ったら、ここで対応づけを書く status: r.status, });
  5. 3. モデルを実装する-規則を定めて振る舞いを作る// 概念:判定結果と理由 export type CancelReason = "fulfillmentStarted" | "alreadyCancelled";

    export type CancelDecision = | { can: true } | { can: false; reason: CancelReason }; // 振る舞い:規則に従って判定結果を出す // 規則:出荷作業(ピッキング)の開始前ならキャンセルできる export const decideCancel = (order: Order): CancelDecision => { switch (order.status) { case "PENDING_PICKING": return { can: true }; case "PICKING": case "PACKED": case "SHIPPED": case "DELIVERED": return { can: false, reason: "fulfillmentStarted" }; case "CANCELLED": return { can: false, reason: "alreadyCancelled" }; default: // statusが増えたらコンパイルエラーで気付ける return order.status satisfies never; } };
  6. 3. モデルを実装する-Containerで組み⽴てる// Container:API呼び出しとモデルへの変換を担う export function OrderDetailContainer({ orderId }: { orderId:

    string }) { const res = useOrderDetailQuery(orderId); // Suspense前提でデータを返す const order = toOrder(res); } return ( <OrderDetail phase={toDisplayPhase(order)} cancel={decideCancel(order)} onCancel={() => cancelOrder(order.id)} /> );
  7. 3. モデルを実装する-判定結果でUIを制御する// Presentational:概念の型だけを受け取り、 statusの値は知らない type Props = { phase: DisplayPhase;

    cancel: CancelDecision; onCancel: () => void; }; const reasonLabel = { fulfillmentStarted: "出荷準備が始まったため、キャンセルできません ", alreadyCancelled: "この注文はキャンセル済みです ", } satisfies Record<CancelReason, string>; export const OrderDetail = ({ phase, cancel, onCancel }: Props) => ( <section> <DeliveryProgress phase={phase} /> <button disabled={!cancel.can} onClick={onCancel}> 注文をキャンセル </button> {!cancel.can && <p>{reasonLabel[cancel.reason]}</p>} </section> );
  8. 4. ルールの所有者を確認する問い 問い キャンセル可否 配送状態の段階表⽰ なぜ?の答え ピッキング以降、棚に 戻すのは労⼒がかかる から 購⼊者がわかりやすいから

    変更に誰の合意が必要か 物流‧配送コスト管理 者 デザイナー 無視したら何が壊れるか 出荷作業 ユーザーのわかりやすさ ルールを所有するか 所有する 所有しない
  9. フロントエンドモデルにするべき対象の抽出⾃動化ができ ていない モデルにしたいのは表⽰の形式ではなく「意味」 変換の性質 例 実装したい場所 文言・ロケール・装飾だけ PENDING_REVIEW → "審査待ち"

    表示制御処理・プレゼンテーション な実装の近く 画面固有の見せ方 PENDING_REVIEW → バッジ色、説 明、ボタン状態 表示制御処理・プレゼンテーション な実装の近く クライアントの概念への意味 変換 PENDING_REVIEW と QUEUED を「処 理待ち」として統合 フロントエンドモデル サーバーの都合を隠し、クラ イアント独自の状態体系を作 る API状態から Editable / AwaitingResult / Completed を 構成 フロントエンドモデル
  10. FAQ

  11. 参考⽂献‧セッション‧記事 DDD on the frontend - Talysson Oliveira Cassiano -

    DDD Europe 「オブジェクト指向UIデザイン──使いやすいソフトウェアの原理」- ソシオメディア株式会社,上野学,藤 井幸多 著 上野学 監修 「ドメイン駆動設計をはじめよう―ソフトウェアの実装と事業戦略を結びつける実践技法」Vlad Khononov 著、増⽥ 亨、綿引 琢磨 訳 「関数型ドメインモデリング ドメイン駆動設計とF#でソフトウェアの複雑さに⽴ち向かおう」 Scott Wlaschin (著), 猪股 健太郎 (翻訳)