フロントエンド(SPA)でオニオンアーキテクチャっぽくデータフェッチとドメインを分離する例
以下のほぼ書きなぐりのスクラップが元koushisa.icon
前提
このページを読みながら参考にしておきたいもの
適用対象の想定
データフェッチしてきた予約メニューを扱う
フロントエンドには見えないが、裏側では複数のエンティティによって成り立っている
フロントエンドから見た時
予約メニューを取得するエンドポイントに1つ対し、ユースケースが複数存在する
単純なリスト表示だったり
セレクトボックスの項目として表示したりだったり
問題
レスポンスの構造とビューの求める構造が一致しない
やりたいこと
コンポーネントとロジックを切り離す
フェッチしてきたデータは当然キャッシュする
code:typescript.ts
// 永続化層
// APIレスポンス<->エンティティの状態をマッピングする
type MenusEntity = {
value: Menu[]
hasMenu: boolean
hasMultiple: boolean
hasContents: boolean
head: Menu | undefined
defaultMenu: Menu | undefined
}
// APIレスポンスを基にエンティティを作成する純粋関数。これはテストする。
const createMenusEntity = (menus: Menu[]): MenusEntity => {
/*~*/
}
// データフェッチを行ってエンティティを返す
// Repositoryパターンと同義
export const menusEntity = selector<MenusEntity>({
key: 'menusEntity',
get:
async ({ get }) =>
const menus = await fetchMenus()
return createMenusEntity(menus)
},
})
---
// ドメイン層
// サービス固有のドメイン層
// エンティティの状態<->ドメインの状態をマッピングする
// ユビキタス言語が使えたら◎だが、言語補完的な意味合いも強い
// エンティティの状態を特定の課題解決のために抽象化したもので、これがアプリの状態制御の核となる
// 例ではタグで区別できるようにした
type MenusDomainModel =
| {
type: 'blank'
}
| {
type: 'single'
menu: Menu
}
| {
type: 'multiple'
menus: Menu[]
}
| {
type: 'skippable'
defautMenu: Menu
}
// エンティティを受け取り、ドメインモデルを返す純粋関数。これはテストする。
const createMenusDomainModel = (menusEntity: MenusEntity): MenusDomainModel => {
const {
value: menus,
head,
hasMenu,
hasMultiple,
hasContents,
defaultMenu,
} = menusEntity
if (!hasMenu && !hasContents) {
return {
type: 'blank',
}
}
if (defaultMenu !== undefined) {
return {
type: 'skippable',
defaultMenu,
}
}
if (!hasMultiple) {
return {
type: 'single',
menu: head,
}
}
return {
type: 'multiple',
menus,
}
}
// 永続化層のエンティティがSelectorで抽象化されていることでキャッシュが効く
// エンティティのユースケースごとに複数のドメイン層を派生させることも簡単
// 規模によってはmenusEntity, createMenusEntityと統合してしまっても良い
export const menusDomainModel = selector<MenusDomainModel>({
key: 'menusDomainModel',
get({ get }) {
return createMenusDomainModel(get(menusEntity))
},
})
コンポーネントは以下のように利用する
code:ts
// ドメインモデルを受け取り、特定のユースケースを達成するビジネスロジック
// ユースケースに依存するのでViewの関心も含んでよい(むしろViewと同じファイルに配置するのがオススメ)
// I/Oが分離されてるのでビジネスロジックを純粋関数化しやすい、テスタビリティが高い
const someBusinessLogic = (menus: MenusDomainModel) => {
switch(menus.type) {
/*~*/
}
return /*~*/
}
// Menusコンポーネント専用のPresentationModel
const presentationModel = selector({
key: "menusPresentationModel",
get: ({get}) => {
// ドメインモデル
const menus = get(menusDomainModel)
return someBusinessLogic(menus)
}
})
const Menus = () => {
// 対象のデータ取得とビジネスロジックをコンポーネントから分離できた
const menus = useRecoilValue(presentationModel)
return <>/*~*/</>
}
解説
createMenusEntity
createMenusDomainModel
someBusinessLogic(必要に応じて)
結合テスト増やしたほうがいいんじゃないとかは模索中
考察
アプリの制御構造と依存関係が分離される
感想
実装例書いたけど、このような構造が必要となる前に考えるべきことがある
フロントエンドにロジック持ち込みすぎていないか?
本来その複雑性はデザインで解決すべきではないか?
基本的には、より上位レイヤで問題解決しておくほうが合理的であると思う
なのでコレは設計パターンとして知っておくといいかもぐらいな感じ
koushisa.iconの結論