Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

サービス開発における フロントエンド・ドメイン駆動設計の実践

32,801 views

Published on

DeNA TechCon 2018 BLUE Stage

Published in: Engineering
  • Be the first to comment

サービス開発における フロントエンド・ドメイン駆動設計の実践

  1. 1. サービス開発における フロントエンド・ドメイン駆動設計の実践 DeNA TechCon 2018 BLUE Stage 2018.02.07
  2. 2. 吉井 健文 システムデザイン本部デザイン戦略部 デザインエンジニアリンググループ   @Takepepe   @takefumi-yoshii 自己紹介
  3. 3. 担当のKenCoMについて 健康リコメンデーションメディア。 健康データの一元管理、利用者の健康度に応じた情報提供。 健康への行動変容を促すサービスへと成長中。
  4. 4. 担当のKenCoMが先日リニューアルリリース
  5. 5. 担当のKenCoMが先日リニューアルリリース アプリから取得した、歩数・体重・血圧 などのバイタルデータを自動連携。 PCブラウザでも閲覧でき、 手動入力による機能を導入。
  6. 6. React + Redux という構成は以前から存在したが、 部分的な機能に閉じており、機能追加で Store 乱立が懸念された。 各エンドポイントで、Single Store を維持しつつ 分割統合を柔軟に行いたい。 リニューアル課題
  7. 7. バイタルデータの表示・編集機能など、 複雑な機能は OOPをもって乗り切る必要性を感じた。 モデルも抽象化も無いRedux に、 OOPデザインパターンを統合できるのか? リニューアル課題
  8. 8. 小粒度の戦略・アイデアを柔軟に反映可能な、 PDCAサイクルを加速させる フロントエンドの基礎を築きたい。 リニューアル課題
  9. 9. リニューアル課題まとめ 1. エンドポイント毎に異なる、Storeの分割統合 2. 複雑な機能に備え、Redux に OOP を持ち込む 3. PDCAサイクルを加速するための基礎構築
  10. 10. 技術選定の背景 - Hexagonal Redux -
  11. 11. 課題領域(ドメイン)を疎結合にし、再利用・分割統合が容易な 最適解を求めDDDに辿り着く。 エリック・エヴァンスのレイヤードアーキテクチャ(※) 導入を試みるも、 Store と モデルが分断され、状態の2重管理が発生してしまった。 (※) エリック・エヴァンスのドメイン駆動設計 アーキテクチャ選定背景
  12. 12. そこから、ヴァーン・ヴァーノンの「実践ドメイン駆動設計」(※) で 紹介されているヘキサゴナルアーキテクチャに出会う。(※) 実践ドメイン駆動設計 イベント駆動で表現されたヘキサゴナルアーキテクチャは、 Redux と相性が良く、多くのコンセプトが一致。 全エンドポイントで Hexagonal Redux を 導入するに至った。 アーキテクチャ選定背景
  13. 13. React + Redux + redux-saga + immutable.js flowtype + jest Hexagonal Redux モジュール構成
  14. 14. ◼ DomainModel ◼ ReduxAction ◼ Adapter (redux-saga) ◼ Adapter (react-redux) Hexagonal Redux 概念図
  15. 15. Hexagonal Redux 概要 1. ヘキサゴナルアーキテクチャを模範とした構成 2. Storeの内側に複数のドメインモデルを構える 3. 「イベント・サービス・モデル」で単体ドメイン扱い
  16. 16. `domains` 配下に「イベント・サービス・モデル」== 「Redux + redux-saga + immutable.js」 `views` には React コード `applications` には エントリーポイント Hexagonal Redux パッケージ構成 ./front/javascripts/ ├── applications ├── constants ├── domains │ ├── achievementQueue │ ├── articles │ ├── healthRecords │ │ ├── stepCounts │ │ ├── weights │ │ └── ... │ ├── recommendedArticles │ │ ├── model.js │ │ └── redux.js │ ├── renderWidget │ │ ├── model.js │ │ ├── redux.js │ │ └── saga.js │ └── ... ├── helpers ├── lib └── views
  17. 17. 抽象化の文脈 - 抽象化へのターニングポイント -
  18. 18. バイタルデータ表示・編集機能 - OOP基底クラス - 項目毎に、表示期間に 応じたグラフリソースの捻 出・不足レコードの fetch
  19. 19. バイタルデータ表示・編集機能 - OOP基底クラス - 表示期間に応じた 平均値の算出
  20. 20. バイタルデータ表示・編集機能 - OOP基底クラス - 該当日のレコードの 選択・更新
  21. 21. バイタルデータ表示・編集機能 - OOPファクトリー - 食前/食後 など、 ドメイン固有の測定値
  22. 22. バイタルデータ表示・編集機能 - OOPファクトリー - 異なるレコードの扱い (1日に複数の測定値)
  23. 23. バイタルデータ表示・編集機能 - OOPファクトリー - 1日に複数の測定値を 保持するレコードの 取り扱い
  24. 24. 基底クラス・ファクトリーのほか、 表示における関心事の分離など、(値の丸め処理・単位ラベル etc) OOPのデザインパターンが活きる場面が多数見受けられた。 また、今後取り扱うバイタルデータが増えることが想定できた。 抽象化の文脈 - 抽象化へのターニングポイント -
  25. 25. 抽象化の文脈 - Redux における抽象化とは? -
  26. 26. ReduxAction を抽象化することで、構成要素の 全てを同一レベルで抽象化することが可能。 ReduxAction 抽象化の立て役となった、 ボイラープレートジェネレーターを紹介。 抽象化の文脈 - Reduxにおける抽象化とは? -
  27. 27. ReduxAction をモデルに委譲する higher-order-reducer を生成。 export function createReducer (commands: string[], namespace: string): Function { return (initialModel: immutable.Record) => { return (model = initialModel, action: ActionCreator) => { const fn = action.type.replace(namespace, '') if (model[fn] !== undefined) return model[fn](action.payload) return model } } } 抽象化の文脈 - Reduxにおける抽象化とは? -
  28. 28. ActionCreators と ActionTypes を生成。 export function createActions (commands: string[], namespace: string) { const types: ActionTypes = {} const creators: ActionCreators = {} commands.map((row: string) => { const type: ActionType = `${namespace}${row}` types[row] = type creators[row] = payload => { return { type, payload } } }) return { types, creators } } 抽象化の文脈 - Reduxにおける抽象化とは? -
  29. 29. ボイラープレートジェネレーターを集約。 このジェネレーターは、状態を変更する Action名を宣言することで、 3種のボイラープレートが一度に出来上がる。(Action名配列の宣言・名前空間の宣言 ) export function createReduxBoilerplate (commands: string[], namespace: string) { const { types, creators } = createActions(commands, namespace) const reducer = createReducer(commands, namespace) return { types, creators, reducer } } 抽象化の文脈 - Reduxにおける抽象化とは? -
  30. 30. 普段の3種のボイラープレート実装は 状態を変化する「コマンド」の StringArray 宣言のみ、 という明快なものとなった。 (要別途 payload 型定義) const { types, creators, reducer } = createReduxBoilerplate([ 'registerAchievement', 'registerAchievements', 'deleteQueueItemByIndex' ], '/domains/achievementQueue/') export { types, creators, reducer } 抽象化の文脈 - Reduxにおける抽象化とは? - 名前空間
  31. 31. export const abstractCommands = [ 'registerHealthRecordsSrc', 'updateHealthRecordsSrc', 'shiftShowRange', 'shiftCurrentDate' ] const { types, creators, reducer } = createReduxBoilerplate([ ...abstractCommands ], '/domains/healthRecords/stepCounts/') const { types, creators, reducer } = createReduxBoilerplate([ ...abstractCommands, 'setEditorCurrentIndex', 'toggleTimingActive' ], '/domains/healthRecords/bloodSugars/') ReduxAction の抽象コード実体は StringArray。
  32. 32. export const abstractCommands = [ 'registerHealthRecordsSrc', 'updateHealthRecordsSrc', 'shiftShowRange', 'shiftCurrentDate' ] const { types, creators, reducer } = createReduxBoilerplate([ ...abstractCommands ], '/domains/healthRecords/stepCounts/') const { types, creators, reducer } = createReduxBoilerplate([ ...abstractCommands, 'setEditorCurrentIndex', 'toggleTimingActive' ], '/domains/healthRecords/bloodSugars/') ReduxAction の抽象コード実体は StringArray。 New New
  33. 33. レコードを登録する レコードを更新する 表示期間を変更する 表示日を変更する ReduxAction の抽象コード実体は StringArray。(コード要約) 歩数のレコードを登録する 歩数のレコードを更新する 歩数の表示期間を変更する 歩数の表示日を変更する 血糖値のレコードを登録する 血糖値のレコードを更新する 血糖値の表示期間を変更する 血糖値の表示日を変更する 血糖値の編集測定点を切り替える 血糖値の測定値表示を切り替える バイタルドメインイベント 歩数ドメインイベント 血糖値ドメインイベント 「歩数の」名前空間 「血糖値の」名前空間 New New
  34. 34. 状態を変化する「コマンド = ReduxAction」 のリファクタが容易である点も、 運用において大きな利点であると言える。 抽象化の文脈 - Reduxにおける抽象化とは? -
  35. 35. 抽象化の文脈 - Redux におけるモデルとは? -
  36. 36. 通常 ReduxStore の状態管理は データソースの保持のみであり、 モデルとしての振る舞いは持ち合わせていない。 ヘルパーモジュールか、view上での処理に 頼らざるを得なかった。 抽象化の文脈 - Reduxにおけるモデルとは? -
  37. 37. この課題を immutable.Record をモデルとして扱う手法で解決。 先ほど宣言した Action が Dispatch されると、 Reducer から委譲された setter/updater が実行され状態が変化する。 export class BloodSugarsModel extends MultiItemModel(props) { setEditorCurrentIndex (value: number): BloodSugarsModel { return this.set('editor_current_index', value) } toggleTimingActive (index: number): BloodSugarsModel { return this.update('timings', timings => { return timings.update(index, timing => timing.toggleActive()) }) } } 抽象化の文脈 - Reduxにおけるモデルとは? -
  38. 38. Immutable.Record は継承可能。 この抽象レイヤーで、先ほど宣言した Action と 同一抽象レベルのメソッドを定義。 export const MultiItemModel = (opt: Props) => class extends ItemQueryModel(props(opt)) { updateHealthRecordsSrc (src: RecordProps): MultiItemModel { const { base_date, end_date, records } = src return this._updateRecords(base_date, end_date, fromJS(records)) } } 抽象化の文脈 - Reduxにおけるモデルとは? -
  39. 39. 「業務処理・ビジネスロジック・表示上の関心事」を継承階層で分離 抽象化の文脈 - Reduxにおけるモデルとは? - XHRレスポンスに応じてレコード生成。 全てのレコードが欲しい。 今表示しているレコードの平均は? 次の表示期間に十分なレコードはあるか? 丸め処理済みのラベルが欲しい。 今表示している期間のラベルが欲しい。 継承 継承 バイタル ドメインモデル (抽象モデル) プレゼンテーション層 ビジネスロジック層 業務処理層
  40. 40. 「基底クラスメソッド・ファクトリーメソッド」のオーバーライド 抽象化の文脈 - Reduxにおけるモデルとは? - XHRレスポンスに応じて 血糖値レコード生成。 全てのレコードが欲しい。 今表示しているレコードの 各測定値平均は? 次の表示期間に十分なレコードはあるか? 丸め処理済みのラベルが欲しい。 今表示している期間のラベルが欲しい。 継承 継承 血糖値 ドメインモデル バイタル ドメインモデル (抽象モデル) 継承 override override override プレゼンテーション層 ビジネスロジック層 業務処理層
  41. 41. OOP デザインパターンを FRP パラダイムに 持ち込むことが可能となった。 課題領域(ドメイン)を分離し、責務に応じて ドメインをスケールさせる、DDD の基礎が出来上がった。 抽象化の文脈 - Reduxにおけるモデルとは? -
  42. 42. コンテキストマップの文脈 - ドメインモデルの粒度 -
  43. 43. ファットモデルを回避するため、 ドメインモデルの課題粒度を細かくした。 粒度が細かいことで、機能同士が疎結合に。 コンテキストマップの文脈 - ドメインモデルの粒度 -
  44. 44. コンテキストマップの文脈 - ドメインモデルの粒度 - バイタルドメイン集約 行動目標記録ドメイン集約 共有ドメイン集約
  45. 45. コンテキストマップの文脈 - ドメインモデルの粒度 - バイタルドメイン集約 行動目標記録ドメイン集約 共有ドメイン集約
  46. 46. ドメインモデルの粒度が細かくなることで、 「横断的関心事」をどの様に参照するべきか、 という点が課題になる。 コンテキストマップの文脈 - ドメインモデルの粒度 -
  47. 47. コンテキストマップの文脈 - 横断的関心事 -
  48. 48. モデル同士が直接参照しあうことは避け、 ドメイン同士が pub/sub する機構上で結合。 モデル外側に位置する「サービス層」に結合点を置くことで、 モデル同士の依存関係は無くなる。 コンテキストマップの文脈 - 横断的関心事 -
  49. 49. コンテキストマップの文脈 - 横断的関心事 - バイタルドメイン集約 行動目標記録ドメイン集約 共有ドメイン集約
  50. 50. コンテキストマップの文脈 - 横断的関心事 - バイタルドメイン集約 行動目標記録ドメイン集約 共有ドメイン集約
  51. 51. サービス層 = redux-saga 非同期処理の middleware として認知されている redux-saga。 ここで実装する継続的コルーチンをサービス層として機能させた。 コンテキストマップの文脈 - 横断的関心事 -
  52. 52. export function * mapRequestStateToUI ({ creators }: { creators: ActionCreators }) { // 無限ループをもって継続的コルーチンを起動 while (true) { // requestQueueドメインを購読する yield take(action => { return action.type === RequestQueueTypes.requestSend || action.type === RequestQueueTypes.receivedSuccess || action.type === RequestQueueTypes.receivedError }) const { requestQueue } = yield select() const isProcessing = requestQueue.isProcessing() // 横断的関心事である XHR処理中という状態を、遷移ボタンを押せなくする状態に変換 yield put(creators.setDisabledUI(isProcessing)) } } 【横断的関心事を結合するサービス】
  53. 53. 【横断的関心事を結合するサービス】(コード要約) 1. XHRQueueドメインイベントを購読  (イベントが発生するまで loop処理は停止) 2. XHRQueueドメインの状態を参照 3.「UIが操作可能か?」という状態に変換 4. 付随ドメインに書き込み 5. 1.に戻る 4. 1. ドメインサービスはドメインモデルの一部であるといえる 2. XHRQueue ドメインモデル ドメインモデル 状態変換 サービス 外部
  54. 54. コンテキストマップの文脈 - サービスの抽象化 -
  55. 55. redux-saga は async/await にデザインが似ており、標準に近い。 分割統合・手続きの差し込み・変更なども容易。 将来的に async/await を利用する様になっても、 設計は変わらないことが期待出来る。 コンテキストマップの文脈 - サービスの抽象化 -
  56. 56. ReduxAction の抽象化はサービス層にも伝播。 継承元が同じドメインは、同名の ActionType を継承。 名前空間が切り分けられているため、 競合することなく手続きを継承する。 コンテキストマップの文脈 - サービスの抽象化 -
  57. 57. 例えば、レコードを fetch するなどの非同期処理。 継続的コルーチン関数起動時に、 ActionTypes / ActionCreators / modelName を注入する。 コンテキストマップの文脈 - サービスの抽象化 -
  58. 58. export function activityGoalLogsSagas () { return [ activityGoalLogsSaga({ types: Daily.types, creators: Daily.creators, modelName: 'activityGoalLogsDaily' }), activityGoalLogsSaga({ types: Weekly.types, creators: Weekly.creators, modelName: 'activityGoalLogsWeekly' }), activityGoalLogsSettingsSaga() ] } 【抽象サービスのコンテキストマップ】
  59. 59. 【抽象サービスのコンテキストマップ】(コード要約) 〜の時、状態を取得する 〜の時、記録を変更する 〜の時、記録を取得する 〜の時、日別行動目標の状態を取得する 〜の時、日別行動目標の記録を変更する 〜の時、日別行動目標の記録を取得する 〜の時、週間行動目標の状態を取得する 〜の時、週間行動目標の記録を変更する 〜の時、週間行動目標の記録を取得する 行動目標記録サービス 日別行動目標記録サービス 週間行動目標記録サービス 「日別行動目標の」名前空間 「週間行動目標の」名前空間
  60. 60. コンテキストマップの文脈 - ドメインクライアント -
  61. 61. ドメインクライアント = View = React ReactComponent の抽象化パターンはこれまで通り。 mapStateToProps・mapDispatchToProps で、 文脈にあった State・ActionCreator を抽象名でマッピングする。 コンテキストマップの文脈 - ドメインクライアント -
  62. 62. Hexagonal Redux では、StateObject の 代わりにドメインモデルをマッピング。 モデル表層(プレゼンテーション層) の存在で、細かな View の出し分けが 不要になる。 課題領域分割で得られる React.render 最適化 【日別】 【週間】 【月間】
  63. 63. export function HealthRecordPanel ({ model }: { model: HealthRecordQueryModel }) { const ctx = 'c-indexHealthRecordPanel' // model の getter で得られる値は、期間分岐・ラベル・丸め処理・添字付与済み return ( <div className={`${ctx}`}> <div className={`${ctx}__upper`}> <p className={`${ctx}__itemLabel`}>{model.getUpperPanelItemLabel()}</p> <p className={`${ctx}__dateRangeLabel`}>{model.getUpperPanelDateRangeLabel()}</p> <p className={`${ctx}__value`} dangerouslySetInnerHTML={model.getUpperPanelValueLabel()} /> </div> <div className={`${ctx}__lower`}> <p className={`${ctx}__itemLabel`}>{model.getLowerPanelItemLabel()}</p> <p className={`${ctx}__dateRangeLabel`}>{model.getLowerPanelDateRangeLabel()}</p> <p className={`${ctx}__value`} dangerouslySetInnerHTML={model.getLowerPanelValueLabel()} /> </div> </div> ) } 【ビジネスロジックが引き剥がされたコンポーネント】
  64. 64. 【ビジネスロジックが引き剥がされたコンポーネント】(コード要約) パネル上部に表示する項目ラベルを取得 パネル上部に表示する日付ラベルを取得 パネル上部に表示する値ラベルを取得 パネル下部に表示する項目ラベルを取得 パネル下部に表示する日付ラベルを取得 パネル下部に表示する値ラベルを取得 モデルに表現力があるため、ReactComponentの仕事は単純
  65. 65. 分岐・フィルタリング・ソートなど、 従来 ReactComponent で行われていた処理は、モデルの責務。 View からビジネスロジックを引き剥がすべきという、 普遍的な最適解が得られた。 課題領域分割で得られる React.render 最適化
  66. 66. ドメインモデルを細かく分割する利点は、 課題領域をシンプルにするだけでなく、 ReactComponent の render 最適化にも繋がる。 render は状態の変化に反応。 各Model が抱える schema が少ない程良い。 課題領域分割で得られる React.render 最適化
  67. 67. 分割統合の文脈 - Queue -
  68. 68. 各種キューイングを課題としたドメインをそれぞれ設けた。 ReduxActionを通じてキュータスクが積まれるため、各々独立している。 【例】Modal / Achievement / Notification / XHR 分割統合の文脈 - 各種キューを司るドメイン -
  69. 69. 表示要素毎にキューを構える。 各種表示キューを取りまとめる親キューの存在により、 要素が被るトラブルを回避できた。 ここでも redux-saga による継続的コルーチンが活きる。 分割統合の文脈 - 表示キューを司るドメイン -
  70. 70. export function * renderAchievementQueue (store: Store) { while (true) { // achievementQueue ドメインモデルを取得 const { achievementQueue } = yield select() // achievementQueue に登録されたの先頭要素を取得 const achievementItem = achievementQueue.getFirstQueueItem() // achievementQueue の要素が無くなったら loop を抜ける if (achievementItem === undefined) break // achievementItem を render yield call(renderAchievementWidget, achievementItem, store) // achievementQueue から 先頭要素を削除 const index = achievementQueue.getQueueItemIndex(achievementItem) yield put(creators.deleteQueueItemByIndex(index)) } // マウント先コンポーネントを空にする disposeReact('[data-react-widget-achievements]') } 【表示キューの継続的コルーチン】
  71. 71. export function renderAchievementWidget (achievementItem: ItemModel, store: Store) { // Promise に紐づけられた React.render。役目を終えると resolve する return new Promise(resolve => { const selector = '[data-react-widget-achievements]' renderReact(selector, AchievementWidget, store, { resolve, achievementItem }) }) } 【Promise で wrap された表示キューアイテム・レンダラー】
  72. 72. 【Promise で wrap された表示キューアイテム・レンダラー】(コード要約) 1. 先頭のキューアイテムを取得 2. キューアイテムがなければ終了 3. キューアイテムの表示 4. 表示が終わるまで待つ( Promise) 5. キューアイテムを削除する 6. 1.に戻る 3. 2.DONE 1. 4. React 表示キュー サービス AchievementQueue ドメインモデル 外部 Promise.resolve()
  73. 73. 分割統合の文脈 - ドメインパッケージ -
  74. 74. 「カラダの記録」や 「行動目標記録」などの機能は 複数エントリーポイントで利用。 ドメインをパッケージ単位で集約。 分割統合の文脈 - ドメインパッケージ -
  75. 75. エントリーポイント毎にモデル層とサービス層は異なるため、 そこで必要なパッケージを統合する。 ヘキサゴナルアーキテクチャ構成要素の内側から 生成・起動・実行する様式は、どのエントリーポイントでも一律。 フレームワークらしき形が表出する。 分割統合の文脈 - ドメインパッケージ -
  76. 76. export const commonReducers = { requestQueue: RequestQueueReducer(new RequestQueueModel()), modalQueue: ModalQueueReducer(new ModalQueueModel()), notificationQueue: NotificationQueueReducer(new NotificationQueueModel()), achievementQueue: AchievementQueueReducer(new AchievementQueueModel()), renderWidget: RenderWidgetReducer(new RenderWidgetModel()), pointAccount: PointAccountReducer(new PointAccountModel()), insurancePointAccount: InsurancePointAccountReducer(new InsurancePointAccountModel()) } 【全エントリーポイントに存在するドメインモデル集約】
  77. 77. export const healthRecordsReducers = { healthRecordsCalendar: CalendarReducer(new CalendarModel()), healthRecordsEditor: EditorReducer(new EditorModel()), healthRecordsSettings: SettingsReducer(new SettingsModel()), healthRecordsStepCounts: StepCountsReducer(new StepCountsModel()), healthRecordsWeights: WeightsReducer(new WeightsModel()), healthRecordsBloodPressures: BloodPressuresReducer(new BloodPressuresModel()), healthRecordsBloodSugars: BloodSugarsReducer(new BloodSugarsModel()) } export const activityGoalLogsReducers = { activityGoalLogsDaily: DailyReducer(new DailyModel()), activityGoalLogsWeekly: WeeklyReducer(new WeeklyModel()), activityGoalLogsSettings: SettingsReducer(new SettingsModel()) } 【機能パッケージ単位のドメインモデル集約】
  78. 78. // create Store const aggregateRoot = extendReducers( commonReducers, healthRecordsReducers, activityGoalLogsReducers ) const store = createReduxStore(aggregateRoot) // run services runRootSaga(commonSagas(store), [ ...healthRecordsSagas(), ...activityGoalLogsSagas() ]) // view scripts applyCommonViewScripts(store) renderAppReactViews(store) 【集約ルートでStore生成、Service層・View層に注入】 集約ルート生成 Store生成 サービス起動 View起動 全エントリーポイントが この様式に
  79. 79. 【集約ルートでStore生成、Service層・View層に注入】(コード要約) /index.js /articles.js /vitals.js /points.js エントリーポイント毎に、必要なドメイン・サービスを採用 共有 ドメイン集約 バイタル ドメイン集約 行動目標記録 ドメイン集約 記事 ドメイン ポイント ドメイン
  80. 80. 【集約ルートでStore生成、Service層・View層に注入】(コード要約) /index.js /articles.js /vitals.js /points.js エントリーポイント毎に、必要なドメイン・サービスを採用 共有 ドメイン集約 バイタル ドメイン集約 行動目標記録 ドメイン集約 記事 ドメイン ポイント ドメイン
  81. 81. 【集約ルートでStore生成、Service層・View層に注入】(コード要約) /index.js /articles.js /vitals.js /points.js エントリーポイント毎に、必要なドメイン・サービスを採用 共有 ドメイン集約 バイタル ドメイン集約 行動目標記録 ドメイン集約 記事 ドメイン ポイント ドメイン
  82. 82. Store の分割統合は容易になったが、 ユーザーステータスなど、初期状態の注入を行う層が必要に。 分割統合の文脈 - ドメインパッケージ -
  83. 83. 分割統合の文脈 - DI of HTML to Store -
  84. 84. 分割統合された Store は初期状態で zero value の状態。 rails に載せるうえで、erb から初期値を取得したい。 この課題を jQueryプラグインライクな vanilla plugin で解決。 plugin 適用時に Store を引数に与えることで、 ReduxAction を Dispatch するDOMに変化する。 分割統合の文脈 - DI of HTML to Store -
  85. 85. export function LoadActionDispatcher (selector, store) { return document.querySelectorAll(selector).forEach(element => { const dataset = JSON.parse(element.dataset.domLoadActionDispatcher) function dispatch (data) { const { type, payload } = data store.dispatch({ type, payload }) } if (Array.isArray(dataset)) { dataset.map(data => dispatch(data)) } else { dispatch(dataset) } }) } 【HTMLにレンダリングされた json を ActionDispatch するプラグイン】
  86. 86. <%= content_tag :div, nil, class: ctx, data: { 'dom-load-action-dispatcher': [ { type: '/healthRecords/stepCounts/registerHealthRecordsSrc', payload: { base_date: @health_record[:base_date], end_date: @health_record[:end_date], src: @health_record['step_counts'] } } ] } %> 【プラグインが適用されるHTML】
  87. 87. 【DI of HTML to Store】(コード要約) /index.js 共有 ドメイン集約 バイタル ドメイン集約 行動目標記録 ドメイン集約 記事 ドメイン ポイント ドメイン /articles.js /vitals.js /points.js サーバーから得られるインスタンスを Load時に Dispatch /vitals/index.html.erb /index.html.erb /articles/index.html.erb /points/index.html.erb
  88. 88. 成果と今後の展望
  89. 89. ReduxAction を抽象化することで、構成要素全てに抽象化が伝搬。 技術選定時の狙いどおり、OOPデザインパターンの恩恵を多く受けた。 ビジネスロジックの再利用で、複雑なユースケースも難なく解を得た。 類似ドメイン間の差異が表面化したため、 新規類似ドメインの受け入れ・機能拡張が容易になった。 KenCoMリニューアルにおける成果 - 抽象化 -
  90. 90. 横断的関心事を結合するサービス層を構えることで、 ドメインモデルが純粋になり、 各種関心事が分離された。 組み替えが容易なサービス層の存在で、 機能の追加変更が容易になった。 KenCoMリニューアルにおける成果 - 分割統合 -
  91. 91. 1. チーム横断でDDDアプローチを発展(脱軽量DDD) 2. 定量的な効果測定を仕込み、PDCAサイクルを加速 3. UI/UX を通じて 事業KPI に貢献(デザイン戦略部メンバー全員の課題) 今後の展望
  92. 92. ご静聴ありがとうございました
  93. 93. Reduxにドメイン層を導入する@Qiita 実装 - Hexagonal Redux - @Qiita MobXは複雑さに耐えられるのか?@Qiita async/await で Modal の Queueing @Qiita redux-ddd-example @github.com immutablejs-record-oop-example @github.com 関連資料

×