フロントエンドDDD
フロントエンド DDD とはフロントエンドの開発に DDD(ドメイン駆動設計)の思想を取り入れた設計思想です。DDD はバックエンドなどの開発では広く活用されていますが、フロントエンドの開発ではあまり普及していません。
一方で弊社(株式会社TAIAN)は婚礼業界向け SaaS のフロントエンドの開発に DDD を取り入れて2年間、100以上の機能を開発してきました。本シリーズでは我々の経験をもとにフロントエンド DDD について説明したのち、チームの変化、大変なところ、今後の可能性などを紹介していきます。
なぜフロントエンド DDD が必要だったのか
フロントエンド DDD について説明する前に、我々がフロントエンド DDD を必要とした背景を説明します。
弊社、株式会社TAIANは婚礼業界向けの業務 SaaS を提供しています。我々は婚礼業界の未来のため、特定の領域・関係者にとどまらず、婚礼業界全体の広範な業務を対象とした SaaS を開発しています。また婚礼業界は様々な伝統や思いやりを背景とする複雑なビジネスルールが存在します。さらに関係者も多く「カップル」「式場」のみならず「参列者(ゲスト)」や「提携業者(パートナー)」などドメインロジック(そのソフトウェアが使われる業務固有のルール)に関わる様々な関係者が存在します。
つまりドメインロジックが複雑なのです。私はいままで様々な SaaS の開発に触れる機会がありましたが、その中で一番ドメインロジックが複雑なプロダクトだと感じています。大変ではありますが、ソフトウェアエンジニアとしてそのような複雑なドメインロジックに向き合うのはとても面白いです。またこのような課題に対する解決策を考えることができれば、それは婚礼業界のみならずこの国の発展に大いに貢献すると考えています。
その解決策の一つがフロントエンド DDD です。プロダクトの使い勝手を追求すると、フロントエンドにもドメインロジックの知識が必要になります。前述のように複雑なロジックを、我々のようなまだそこまで大きくない組織が把握・管理・制御するには、ドメインロジックを実装詳細から抽出できる DDD の思想をフロントエンドにも取り入れることが必要だと考えました。
DDD とは
DDD とは Domain-Driven Design の略でソフトウェアの設計思想の一つです。設計思想なので具体的な技術や実装方法を強制するものではないです(とはいえベストプラクティスやパターンは存在します)。DDD とは「ビジネスの問題領域(ドメイン)を深く理解し、その本質的な概念やロジックを抽出して、現実世界と対応付けたモデルを構築する設計思想」です。この思想を実現するために 「エンティティ」や「レポジトリパターン」などの設計手法が提案されていますが、その本質はドメインロジックへの深い理解とコードへの接続、それを実装詳細から切り離して表現することです。
次の章で具体的な話をします。
フロントエンド DDD とは
フロントエンド DDD はフロントエンドに DDD の思想を取り入れた設計思想です。
たとえば、結婚式に出席した参列者にお酒が提供できるかどうかを画面で表示する場合を考えます。婚礼業界に詳しい人(ドメインマスター)に聞いたところ以下の回答を得ました。
ドメインマスター
「参列者が満20歳以上であり、お車でお越しいただいていなければ、お酒を提供できます。」
(※説明のための例です。)
このロジックをフロントエンドで実装する場合、以下のようなコードが書かれるかもしれません。
const Panel = () => {
const [user, setUser] = useState<{ age: number, arrivalMethod: string } | undefined>(undefined)
return (
<div>
<Form onSubmit={v => setUser(v)} />
{/* ユーザが20歳以上か、来場手段が車かどうかをチェック */}
{user.age >= 20 && user.arrivalMethod !== "car" && <button>この方にはお酒の提供が可能です</button>}
</div>
)
}
しかしこのコードでは規模が大きくなり、ドメインロジックが複雑になり、実装担当者が流動的に変わる場合、以下のような問題が発生します。
-
User
の定義が統一されていない状況であり実装者によって意味するものが異なる。たとえば別の実装者は「参列者」ではなく「カップル」や「管理者」を意味すると勘違いするかもしれない。 -
user.age >= 20 && user.arrivalMethod !== "car"
というロジックが複数箇所に散らばり、修正時に対応が漏れるなどの問題が発生する。例えば「お車でお越しになっているが、代車の手配があるなら提供が可能とする」と追加の修正があるかもしれない。 - 「お酒の提供可否」という重要な条件をテストするために多数のパターンに対して UI の重厚なテストが必要になる。
これを DDD を用いると以下のような表現になります。
class Guest extends Model {
constructor(args: Args) {
// 初期化処理はよしなに
// guest が満たすべき制約などのチェックもここで行う
}
// 複雑な条件は guest ドメインモデルの中に隠蔽する。
canBeServedAlcohol = () => user.age >= 20 && user.arrivalMethod !== "car"
}
const Panel = () => {
const [guest, setGuest] = useState<Guest | undefined>(undefined)
return (
<div>
<Form onSubmit={v => setGuest(new Guest(v.age, v.arrivalMethod))} />
{/* UI 部分は「お酒が提供できるかどうか」のみに関心があるので canBeServedAlcohol のみ呼び出せば良い */}
{guest.canBeServedAlcohol() && <button>この方にはお酒の提供が可能です</button>}
</div>
)
}
{/* 例えば一覧でも同じロジックが呼び出せる */}
const List = ({ guests }: Guests) => (
<ul>
{guests.map((guest) => (
<li key={guest.id}>
{guest.canBeServedAlcohol() ? 'アルコール可' : 'アルコール不可'}
</li>
))}
</ul>
)
// テストもドメインモデルに対して行える
describe('アルコール提供について', () => {
it('20歳以上かつ来場手段が車ならアルコール提供が不可能', () => {
const g = new Guest(20, 'car')
expect(g.canBeServedAlcohol()).toBe(false)
})
it('年齢が20歳未満ならアルコール提供が不可能', () => {
const g = new Guest(19, 'car')
expect(g.canBeServedAlcohol()).toBe(false)
})
it('年齢が20歳満以上来場手段が車以外ならアルコール提供が可能', () => {
expect(new Guest(25, 'walk').canBeServedAlcohol()).toBe(true)
expect(new Guest(30, 'bus').canBeServedAlcohol()).toBe(true)
})
})
以下のような変化がありました。
- 「参列者」という単語の定義をドメインマスターと煮詰め
Guest
という命名した。この命名はプロジェクト全体で徹底して使われるため、実装者が「参列者」を「カップル」などと誤解することがなくなった。 -
user.age >= 20 && user.wantsAlcohol
というロジックをGuest#canBeServedAlcohol
というメソッドに隠蔽した。これによりこのドメインロジックはGuest
クラス内に閉じ込められ、他の箇所での修正漏れがなくなった。 -
Guest#canBeServedAlcohol
というメソッドを単体でテストすることができるため、UI の重厚なテストが不要になった。
ここまで見て、同様のことは DDD を意識しなくとも、適切にロジックを関数に切り出すことでも実現可能のように思えます。
たしかに十分実現可能です。しかし DDD の概念に忠実に従う場合は、インターフェースが呼び出し元に依存しないようにしたり、処理の責務を明確にできたり、制約を表現する力が組織に働きます。もちろん、これらはクラスベースではなく関数ベースでも実現可能です。
上記で紹介した内容は弊社のフロントエンド DDD のほんの一側面です。このような書き方を実現するために様々な工夫が必要です。弊社ではフロントエンド DDD の概念を提唱した社内のメンバーの名前を取って「Yagyu.js」と呼んでいます。Yagyu.js はフロントエンド DDD を実現するための詳細な設計方法およびそれを支援するライブラリです。TypeScript で DDD で必要なクラスベースの実装をするための様々な工夫がなされています。
フロントエンド DDD でプロダクトを構築することを検討されている場合は有益な情報かと思います。Yagyu.js の詳細については別記事でご紹介します。
フロントエンドのドメインロジックとは
ところでフロントエンド DDD のドメインロジックとはなんでしょうか?通常バックエンドなどで用いられる DDD ではドメイン層を他に依存しない唯一無二の存在として扱い、UI や DB など外界とやり取りする部分にはドメインロジックは宿りません。しかし我々が今考えたいのは UI であるフロントエンドのドメインロジックです。ここはフロントエンド DDD の難しい点ですが、弊社では「実装技術によらない画面の表示・操作に現れるドメインロジック」と定義しています。
たとえば「参列者一覧」から「お酒を提供できる参列者一覧」を抽出してセレクトボックスで表示する処理を考えてみましょう。以下のようなコードが書かれるかもしれません。
const List = (guests: Guest[]) => {
return (
<MySelects options={guests.filter(guest => guest.age >= 20 && guest.wantsAlcohol).map(guest => ({ value: guest.id, label: guest.name }))} />
)
};
決して悪くはないですが DDD の観点からすると詳細実装にドメインロジックが埋もれているように見えます。
以下のように書き換えてみましょう。
const List = (guests: Guests) => {
return (
<MySelects options={guests.onlyCanBeServedAlcohol().toOptions()} />
)
};
Guests
は Guest
の集合を表すクラスであり、Guests#onlyCanBeServedAlcohol
は「お酒を提供できる参列者」を抽出するメソッドです。Guests#toOptions
は MySelects
で使えるようにオプションに変換するメソッドです。このコードは一見するとドメインロジックが抽出され完璧に見えます。
しかし toOptions
はやりすぎです。フロントエンドのドメインロジックとは「実装によらない画面の表示・操作に現れるドメインロジック」です。しかし toOptions
は HTML のオプションに変換するための実装詳細です。これは実装の都合に影響されるロジックであり、ドメインロジックではありません。ドメインロジックに詳しいドメインマスターは「お酒を提供できる参列者を抽出する」とは言うかもしれませんが「お酒を提供できる参列者を HTML のオプションに変換する」とは言わないでしょう。
弊社のフロントエンド DDD では以下のように書かれます。
import { generateOptions } from '~/ui/utils';
const List = (guests: Guests) => {
return (
<MySelects options={generateOptions(guests.onlyCanBeServedAlcohol())} />
)
};
あくまでドメインロジックを表すドメイン層の責務は「お酒を提供できる参列者を抽出する」だけです。それを HTML のオプションに変換するのは UI の責務です。その責務は適当な utils にまかせています。
チームの変化
私はソフトウェアアーキテクチャの中核はソースコードではなく人間だと考えます。そのためフロントエンド DDD を語るうえではチームの変化について語らなくてはなりません。DDD はチームがつくるソースコード自体にも影響を与えますが、チームにも大きな影響を与えます。なお「ドメインが実装から隔離されてメンテナンス性が上がる」のような実装に関わる部分は既存の DDD のメリットで紹介した通りなので再度触れません。
ドメインロジックを重要視するように
フロントエンド DDD を導入するまでは、すべての情報は UI の状態を表現するための部品という扱いでした。そのためドメインロジックを軽視してしまうことが多かったです。
わかりやすいのはテストです。フロントエンド DDD によりクライアントサイドの重要なロジックのテストが書きやすくなります。その結果、チーム内でドメインのコードを書くならテストも必ず書く(書かないといけない)という意識が芽生えます。フロントエンド DDD を導入するまでは、計算プレビューなど重要な箇所であっても、膨大な UI のコードの中に隠れて見逃されていました。それがフロントエンド DDD により嫌でも露出するとテストを書かざるをえなくなります。
価値観の統一による不毛な議論の削減
フロントエンド DDD を導入するまでは、様々なコードの書き方の価値観が混在していました。その結果人によってコードの表現方法がことなり、同じ議論の繰り返し・ぶり返しが発生し、レビューでも価値観のすり合わせが行われるなど不毛な(?)議論が多発していました。
しかしフロントエンド DDD を導入後は DDD が強力な共通の価値観になりました。あらゆるフロントエンドの書き方に対して「それはフロントエンド DDD の観点からどうか?」という視点が第一に入るようになりました。その結果、前提の価値観、つまり我々が最も大事にすることが揃ったうえで適切な議論がなされるようになりました。
レビューの心理的負荷の低減
フロントエンド DDD を実現するために我々は書き方を統一しました。それによってレビュー時にコードの全体像を把握しなくても、どこになにがかかれているかが事前にわかるようになりました。そのためプルリクエストのレビューをするときに「まずはコード全体を把握しなくてはいけない」という心理的負荷が減りました。
またコードの変更ディレクトリを見れば UI の些細な変更なのか、それとも我々のコア価値であるドメインの変更なのかがわかるようになりました。そのためプルリクエストの性質に応じてどれぐらい集中してみるべきかを調整することが容易になります。
大変なところ
とはいえフロントエンド DDD は完璧な概念ではないです。アプリケーションの性質によってはまったく役に立たないこともあります。フロントエンド DDD は基本的にクライアントサイドに複雑なドメインロジックがあるアプリケーション向けです。またパフォーマンスや UI/UX を極限まで追い求めるような性質のアプリケーションも、クライアントサイドのデータ構造がそれらに強く影響を受けるため、不向きです。
フロントエンド DDD は抽象化コストのために記述量が増えて負担になることがあります。さらに全メンバーに DDD という概念を浸透させるのも大変です。新メンバーの受入時のオンボーディングコストも増え、また「表示のロジック」というまだ確立していない概念を議論するコストも増えます。
さらに我々のサービスであってもすべての画面が複雑なドメインロジックがあるわけではないです。むしろ我々であっても、多くの画面が単純な CRUD です。それでも一部の画面や概念がとても複雑になることがあります。そのときにフロントエンド DDD は効力を発揮します。逆に考えれば、それ以外の箇所では書き方を揃えるために過剰な抽象化コストを払う必要があります。
我々は普段の書き方に追加の抽象化コストが掛かったとしても、一部複雑なドメインロジックが発生した場合に備えてフロントエンド DDD を選択しました。我々のようなサービスではある日突然、全然関係ない画面に複雑なドメインロジックが侵食してくる可能性があります。例えば「商品」という複雑なロジックが「お気に入り」という概念を通じて「結婚式を上げるカップルアカウント」の画面の操作の事情に露出することがあります。そのような場合に備えれるよう、すべての画面で抽象化コストを払いフロントエンド DDD を採用しています。
フロントエンド DDD の可能性
コア価値であるロジックがフロントエンドの技術変遷の影響を受けすぎない
ドメインロジックはプロダクトのコア価値です。特に我々のような Vertical SaaS のプロダクトの仕様は、様々なお客様に使われていく中で磨かれながら育てられていきます。つまりドメインロジックは会社の重要な資産です。
一方で、フロントエンドの技術は移り変わりが激しいです。最適な体験をお客様に届けるには状況に応じてコードベースを変更し、別の技術に乗り換える必要性があります。
フロントエンド DDD にはこの矛盾を解消することを期待しています。我々はまだコードベースを変更するほどの技術変更はありません。しかし今後技術変更が起こり得るときに、コア価値であるドメインロジックは維持しつつ、配下の要素技術のみをスムーズに変更することができたら良いと考えています。
生成AIの作業領域との分離
現状、生成 AI は一般的な技術知識に対して精度良い出力をします。一方で仕様や既存の複雑なドメインロジックを理解して適切な変更を加えることは苦手です。
フロントエンド DDD により一般的な技術知識と個社固有のドメインロジック部分が分離されていれば、生成 AI による変更範囲を明確に一般的な知識部分に制限することが容易になります。またドメインロジックの表現範囲を容易に区別できるので、既存のドメインロジックをコンテキストに指定することが容易になります。
もしドメインロジックが分離されていなければ、生成 AI が出力する一見まともなコードの中から、重要なドメインロジックの変更が紛れ込んでないかを注意深く確認する必要があるかもしれません。
最後に
フロントエンド DDD は銀の弾丸ではありません。しかしアプリケーションの性質によっては強力な手段になりえます。
我々が相対する婚礼業界を始め、この国には複雑なドメインロジックにより上手くデジタル化の恩恵が受けられなかった業界が多くあります。
フロントエンド DDD はそれに対する一種の挑戦です。
We are hiring!
TAIANでは、このような開発・技術・思想に向き合い、未来をつくる仲間を一人でも多く探しています。少しでも興味を持っていただいた方は弊社の紹介ページをご覧ください。
Discussion
こんにちは。興味深い記事をありがとうございます。用語の定義もしっかりされていて、高いレベルで運用されていることが伺えました。
1点質問なのですが、バックエンドはどのような処理をしているのでしょうか?
例えばSupabaseのようなAPIとDBを一体化したようなバッグエンドだとこのアプローチと相性がいいように思いますが、もしバックエンドでも同じようなクラスを作っていると大変になるかもなと思いまして。
あるいはバックエンドもTypeScriptであり、ドメイン層は同じコードを使っているのでしょうか?
(返信欄を誤ったため再投稿)
コメントありがとうございます!
バックエンドは Ruby on Rails を用いており、フロントエンドはバックエンドと GraphQL でやり取りしています。
ここはフロントエンド DDD を導入する前から既にあった仕組みです。
おっしゃるとおり、バックエンドと似たようなモデルがフロントエンドでも書かれることが多いです。
(Ruby on Rails のモデルはドメインモデルとは言い切れないので「ドメインモデル」ではなく「モデル」と表現します)
記事中に記載したとおり、我々のプロダクトは式場、カップル、提携業者(パートナー企業)のように対象ユーザの属性ごとに画面が別れております。そのため同じバックエンドのモデルに対して、各属性向けの画面のフロントエンドのモデルは微妙に異なることが多いです。つまりバックエンドとフロントエンドのモデルが 1:1 対応しないので、我々の場合はバックエンドとフロントエンドで似たようなモデルを書くメリットがありました。
なるほど、フロントとバックエンドで似ているが違うモデルを作られているのですね。
この構成でのバックエンドの処理が理解できました。ご返信ありがとうございました。
はじめまして。興味深いタイトルだったので、拝見しました。
文脈的に、フロントエンドでのビジネスロジックは、サーバー関数としてではなく、純粋にブラウザで処理されているものかと見えました。
となると、改ざんなどの悪用リスク対策はどうされているのか気になりました。
フロントだけですと、開発者ツール使えば、如何様にでも変更できます。
例えば、改ざんされたデータをPOSTすると、バックエンドでも専用のバリデーションなどがないと、意図しないデータがCRUDされ、整合を保てなくなると思います。
前の方のコメントでも取り上げているように、バックとフロントが1対1対応でないとなると、フロントだけだと不十分で、どのようにデータの整合を保っているのか気になった次第です。
コメントありがとうございます!
フロントエンドからの入力は信用せず、バックエンドで検証しています。
その上で UX 向上など観点でフロントエンドにもドメインモデルが表現されています。
役割ごとに画面が別れているというサービスの性質上、バックエンドから見ると1つのドメインモデルであっても、フロントエンドから見た「カップル(新郎新婦)」向けのドメインモデルと、「パートナー(提携業者)」向けのドメインモデルは異なる場合があります。
例えば、どちらかには表示してされてはいけない内容があったり、どちらかからしか変更できないように制限されているものもあります。そのためモデルが別れており、このことを「1対1対応しない」と表現しておりました。
なるほど、POST特に検証自体をバックエンドが担っているであれば、
クリティカルな問題は抑えられそうに見えました。
ご回答ありがとうございました!