【後日追記】 この記事ではドメインモデリングのHowは特に触れてないです。 フロントエンドのドメインモデルとはWhatなのかを書いています。
———
最近フロントエンドのコードを作る時間が殆どになったのと、AIが生成したコードの品質を担保するのが仕事になった。 フロントエンドのコードを作るうえで、人間のエンジニアとしてやるべきこと・やる価値があることはなんだろうかと考えてるうちの一つにフロントエンドのドメインモデリングがある。
フロントエンドにも固有のドメインはある
ドメインモデリングについて本や記事を読むと多くはバックエンドの処理を前提に語られてる。 モデリング対象の例も、バックオフィスの業務が多くて、ドメインエキスパートもバックオフィスのスタッフを対象にした記事が多いように見える。 フロントエンドのモデリングの話も少しあるけど、バリデートされた値か否か~という文脈かが多いイメージ。
でも、フロントエンドにもバックエンドで語られるようなドメインモデルはあって、それはバックエンドとは違う形をしていることも多くあるはずという話。
UIを決める人(デザイナーやカスタマーサポートなど会社によりけり)は、背後のバックオフィスや内部のコードがどう動いているかを知らなくても、画面から見える情報をもとに、ユーザーがどういう状態にいて、次にユーザー何をしてほしいのかを想像する。
このときUIを決める人が思い描く状態やフローは、バックエンドが管理しているドメインとは違う粒度や用語で扱われることがある。 その粒度や用語が、フロントエンドが扱うべきドメインだと考えてる。
(フロントエンドのドメインモデルというのか、ビューモデルと呼ぶのかBFFの延長と呼ぶのかが正しいかはちょっとわからんのだけど、、 兎に角、UI側に「捉えるべき概念」が存在していて、それを説明するために"フロントエンドのドメインモデル"という用語をつかうことにしている。)
具体的にフロントエンドのドメインモデルとはどういうものなの
実務で出会ったケースをちょっと汎化して、配送状態を扱うシステムの例に置き換えて紹介してみる。
このシステムのUIには、「発送前か、発送済みか」で表示を出し分ける箇所があるとする。 配送追跡のリンクが発送後にだけ見えるといったもの。 ユーザーが気にしているのはもう発送されたかどうかだけ。
一方、バックエンドは配送の状態をもっと細かく管理している。
type ServerDeliveryStatus = | "LABEL_CREATED" | "PACKING" | "SHIPPED" | "IN_TRANSIT" | "OUT_FOR_DELIVERY" | "DELIVERED"
UI側が必要としているのは2つの状態だけなのに、バックエンドはこれだけの状態を持っている。バックエンドの扱う状態と粒度が違う。
素朴にAPIから受け取ったステータスをそのまま分岐条件に持ち込んだとする。
const isTrackingVisible = deliveryStatus === "SHIPPED" || deliveryStatus === "IN_TRANSIT" || deliveryStatus === "OUT_FOR_DELIVERY"
これでも正しく動くが、問題を抱えている。
この分岐が本当に気にしているのは「発送前か、発送済みか」でしかない。バックエンドの状態を6種類すべて知る必要はないのに、実装はその粒度を引きずってしまっている。画面を見る人の頭の中にある「発送済みだから追跡できる」という素朴な概念が、コードのどこにも形として存在しない。
そこで、表示制御に入る前に、バックエンドの状態をUI側の業務概念へ変換する。
type ShippingPhase = "BEFORE_SHIPPING" | "AFTER_SHIPPING"
function toShippingPhase(status: ServerDeliveryStatus): ShippingPhase {
switch (status) {
case "SHIPPED":
case "IN_TRANSIT":
case "OUT_FOR_DELIVERY":
case "DELIVERED":
return "AFTER_SHIPPING"
case "LABEL_CREATED":
case "PACKING":
return "BEFORE_SHIPPING"
}
}
表示制御はこの概念の上に乗せる。
const isTrackingVisible = toShippingPhase(status) === "AFTER_SHIPPING"
こうすると、バックエンドに新しい配送状態が追加されたときにやることは、「それは発送前扱いか、発送済み扱いか」を一度判断するだけになる。toShippingPhase の switch を一箇所直せば終わりで、個々の表示制御には手を入れなくていい。
「整理」ではなく、変更影響範囲を閉じ込める活動
toShippingPhase を作ったのは、変更の影響範囲を、UI側の概念の単位に区切りなおすのが目的。
たとえば上記のUI表示制御とは別の部分で運用が変わって、「OUT_FOR_DELIVERY(配達中)は、配達が完了するまでは発送済みとして扱わず、発送前のフェーズに戻したい」という要望が出たとする。
要望起案者からすると、これは小さい変更に見える。概念のうえでは「OUT_FOR_DELIVERY を AFTER_SHIPPING から BEFORE_SHIPPING に付け替える」だけの話で、個々の表示制御に影響が出てると思わない。(配送中か配送前かで制御されてる部分が細かい配送状態と同一概念とは思えない)
function toShippingPhase(status: ServerDeliveryStatus): ShippingPhase {
switch (status) {
case "SHIPPED":
case "IN_TRANSIT":
case "DELIVERED":
return "AFTER_SHIPPING"
case "LABEL_CREATED":
case "PACKING":
case "OUT_FOR_DELIVERY": // 配達完了まで発送前扱いにする
return "BEFORE_SHIPPING"
}
}
物理的な実装で言うとcase を一行動かすだけで終わる。
表示分岐に関しては発送済みなら表示するというロジックに変更はない。
でも、もし最初の素朴な実装のまま、status === "OUT_FOR_DELIVERY" の判定がUIのあちこちに散っていたら↓のような悪影響が出る。
- 影響範囲を洗い出さないといけない
- 条件が追加された場合の判定結果がデザインの意図どおりかを確認しないといけない
業務上は「分類をひとつ付け替えるだけ」の小変更なのに、実装上は「影響範囲の広い変更」「デザイナーを巻き込んでデザイン意図を確認しないと判別つかない変更」になってしまう。
この認識のズレが多いと、ビジネスに詳しいロールの人が、シシテムの変更案を検討して提案したけどエンジニアの見積が出たらコスパが合わないみたいな状態を起こす。 結果、事業の意思決定を阻害したりシステムの変更速度を下げる。
フロントのドメインを正しくコードに反映していれば、業務側が感じる変更の大きさと、実装側が引き受ける変更の大きさが一致する。 「配送済みの判定変えます」という一語で、両者が同じ規模感を共有したい。
生成AI時代に、人間が担うべき部分
最近はコーディングの多くを生成AIに書かせるようになった。
それらしい境界を引くことはAIにもできる。状態名の意味を推測して、もっともらしい分類を提案することはできる。 でも、それが正しい境界かどうかを決める根拠は、コードの中にはないのでここの最終意思決定は人間が担いたい。
境界の正しさは、モデルに関わるステークホルダーとの会話や、その人たちしか知らないコンテキストの側にある。AIはそこに能動的にはアクセスできない。だから、それらしい境界は引けても、正しい境界を引くことはできない。
ここが、生成AI時代に人間が引き受けるべき部分じゃないかなあ... どこの場で、誰の会話を聞いて、どの概念で整理して、どの変更影響範囲に閉じ込めて、どの責務でUIを分けるか。これはコードの外にある文脈を、コードの内側の構造に翻訳する仕事で、文脈を持っている人間にしかできない。
(コードの内側の構造ではなく、自然言語のドキュメントで残すってことも今ならできるけど、せっかく型とかで安全に意図を残す術があるのに自然言語に委ねるメリットは自分はあまり感じてない)
よりより正しい規模感でより安全にシステムを変えられる状態を作ること。それは、エンジニアが事業に対して担える役割のひとつだと思う。



