DMMグループの一番深くておもしろいトコロ。
テクノロジー

redux (redux-saga) の設計にシーケンス図を用いてみた話

DMMグループの一番深くておもしろいトコロ。

はじめに

こんにちは! プラットフォーム開発部、webエンジニアの千葉です。
デザイン〜フロントエンドを軸に、バックエンド〜インフラも含めてできることは何でもやっています。
そしてできることをもっと増やしたいと思う日々です!

さて、今回の記事では、react - redux を用いたフロントアプリケーションの設計の際に、 Plant UML を使ってシーケンス図を作成してみたお話をしようと思います。

※ 本記事は react - redux の知識・技術がある方に向けた内容になっています。ご了承下さい。

経緯

react - redux のアプリケーション設計は煩雑になりがちで、特に非同期処理のビジネスロジックをどこで行うか、web 上で様々な議論が見受けられます。
今回自身も API 駆動の next.js を用いたフロントアプリケーション開発を行うことになり、react - redux の設計をシンプルにできないか考えたのが発端です。

結果として、redux-saga を使用し saga task にビジネスロジックを集中させ、その内容をシーケンス図で簡潔に表現してみた内容をお話します。

使用したフレームワーク・ライブラリなど

今回のアプリケーション開発で使用したライブラリとして redux-saga の他に

  • next.js
  • next-routes
  • redux-form

があり、本記事にライブラリの API が一部登場します。
ただし、API の機能がどうこうという話は出てきません。

設計の方針を決める

Action, Reducer に一切ビジネスロジックを持ち込まない設計にする

アプリケーションの規模にもよるとは思いますが、redux でビジネスロジックを記述する際には、view からのデータを action で加工(redux-thunk では action で非同期処理を行いますよね)して reducer に渡す、もしくは reducer でデータを加工してしまうなど、いずれかに寄って肥大化しがちです。

もちろん、設計方針を決めることである程度の制御・整頓はできるのですが、ビジネスロジックを action や reducer に寄せ過ぎてしまっては、 それぞれに本来の役割ではない処理までも書いてしまうことになるなぁと思っていました。

考えた結果、今回 action, reducer の仕事を明確にすることで、middleware でビジネスロジックを完結し、非同期処理などの副作用も middleware で吸収するという結論に至りました。

そのうえで、採用したのが redux-saga になります。

Action, Reducer はただ値を返す関数

Action

  • Action 名は『実際に起ったこと』を命名する
  • Payload すなわち値の操作はしない
  • Action は『redux-saga のタスクを起動する』または『reducer に action type と payload を引き渡す』仕事だけをさせる

Reducer

  • 各 action に対してどの state を更新するかの仕事だけをさせる
  • Action から渡ってくる payload すなわち値の操作はしない

上記をまとめると以下の設計方針になります。

  1. Action はアプリケーション内で起こるイベントに近い命名にする(値を渡す・値を取得する・前のページに戻る等々)
  2. Reducer は state の値を更新できる唯一の関数とする
  3. 両者とも値の操作は行わない

Saga task での制約

  • Action で必ずタスクを開始する
  • Action でタスクを終わらせる。または Routing 処理でタスクが終わるようする
  • 非同期処理は call API を使う
  • Payload - 値の操作ができる(以下の約束を守ること)
    • State から値を取ることは可能 (select API を使う)
    • State から取り出した値をタスク内で操作することも可能
    • ただし、state の更新は action => reducer 経由で必ず行う(操作した値で直接 state を更新しない)

これより、redux-saga の task 内に処理を集中させることができます。
以下イメージになります。

Saga task をどう作っていくか

仕様と設計方針が決まり、いざ実装となった時、様々な処理が絡み合う saga task をいきなり実装するのはなかなか大変と感じました。
そこで本題である、saga task の処理を図式化してみる試みを行いました。 今回はドキュメントからシーケンス図やクラス図作成もできる Plant UML を使用しました。

https://plantuml.com/ja/

Plant UMLで処理フローを描写する

Saga task で仕様に沿った処理が行われる時、アプリケーション上にどういう登場人物がいて、どういう関係性をもって処理が進むのかを軸に図式化を行いました。以下が処理フローに登場する人物です。

通常の UML のパーツに、アプリケーションの登場人物を当てはめています。

actor Client <<user>>
participant View
control Task
collections Reducer
database State
participant API

これを実際に図に変換すると、

となります。 この順番も先の方針に沿っており、

  • Client <<user>> <=> View : ユーザが View に対しての操作
  • View <=> Task: View からの Action によって起動される saga task の関係
  • Task <=> Reducer: saga task から Action 経由で Reducer に値を渡す場合の関係
  • Reducer <=> State: Reducer 経由で更新される State の関係
  • API はアプリケーションの外側に位置するため一番外側に配置

という関係性を視覚化しました。アイコンなどはそれっぽい表示にしたいだけなので、特に登場人物との関連性はありません。

次に仕様の例として、フォーム画面にてユーザーが内容を入力し、入力情報を API を使用して登録する処理フローを例に Plant UML に落とし込んでみましょう。

UML に処理を書いた例がこちらになります。

@startuml
title サービス情報入力時の処理

actor Client <<user>>
participant View
control Task
collections Reducer
database State
participant API

group ユーザの情報入力
    Client -> View: フォームに値を入力
    View -> Reducer: Action\n任意のredux-formのAction
    Reducer -> State: Reducer\nフォーム要素の値更新
    hnote over State #fffacd: form.registData
    State -> View: フォーム要素の値更新反映
    View -> Client: フォームに値が反映される
end

group 入力された情報の確認画面への遷移
    Client -> View: 「登録する」ボタンを押す
    View -> Task: onSubmit\nactions@postRegistData(params)\nPOST_REGIST_DATA,params
    activate Task #ef857d
    hnote over Task #a3d6cc : hundleRequestPostRegistData
    Task -> View: saga@put\nstartSubmit
    activate View #ffe9a9
    hnote over View #ead7a4 :『登録中』のポップアップ表示
    Task -> API: saga@call\nPOST /regist\nbody: JSON.stringify(params)
    alt Create 201
        API -> Task: response status 201 craeted
        Task -> View: saga@put\nstopSubmit
        deactivate View
        Task -> View : saga@put\nreset
        hnote over View #ead7a4 : form要素のリセット
        Task -> View: next-routes@pushRoutes\nservice/list
        deactivate Task
        hnote over View #ead7a4 : サービス一覧へ遷移
        View -> Client: GET 200 service/list
    else WIP faild 4XX or 5XX
        API -> Task: error
        Task -> View: saga@put\nstopSubmit
        Task -> View: saga@put\nsetSubmitFailed
        View -> Client: 登録エラーのポップアップ表示
    end
end

@enduml

redux-form などでフォームの入力処理や State の管理をやってくれる部分は簡潔に記述しています。
肝となるのは、入力情報の登録フローです。

 

このように実際の処理がどのような関連性を持って進むのか視覚化できるので、仕様が把握しやすくなります。
特に API を使用する場合は「登録中」などのポップアップの表示や、redux-form など他の react のライブラリを使用している場合の API をどこでどう使うのかを事前に掌握した状態で実装に移ることができ、工数も削減できました。

UML を書くうえで気をつけたこと

自分の考えた仕様を UML に書くだけではなく、実装やチームでの共有を踏まえていくつか書き方を統一しました。

1. どういう図式が伝わりやすいか考える

今回は別のエンジニアさんがシーケンス図の話をしている時に「saga task の処理フローがシーケンス図で描写できる」と思い、試したところしっくりきたのでシーケンス図を採用しました。
仕様とプログラミングの中継ぎのような存在にしたいので、ドキュメントの記載方法や図式だけが孤立してしまう方法を採用しない方がいいかもしれません。

Saga task に関しては、各々の Saga task を個別のPlant UML => シーケンス図として表現できる特性によって、仕様とプログラムの効果的な中継ぎになれたと思っています。

2. Action, Reducer や Saga task の命名を図の作成時に決める

命名法を実装時に悩むのではなく、先に決めてしまいましょう。 この時点で名前の重複や、命名規則のバラつきなどに気づければ、実装後のリファクタもしなくて済みます。

3. どの API を利用するか記述する

Saga task での処理は redux-saga の API を使用して Action や API の呼び出しを行います。
今回は redux-saga を初めて使うということもあり、どの処理段階でどういう API を使用するのかを理解することで、実装時のライブラリ機能のもれをなくすことができました。

4. クライアント(ユーザ)の行動で処理を分ける

今回の例になりますが、「情報を入力する」ことと「入力した情報を登録」することは別の事象です。
もちろん、一連の UX として記述することもわからなくはないですが、プログラム上では処理が別々になりますよね。

よって、ユーザの行動を細かく分割するという作業も事前にやっておくと、各々の処理が疎(そ)になるので見通しが立てやすくなります。

所感

今回わりと思いつきで動いてみたことながら、収穫は2つほどあったと思います。

  1. Redux など登場人物が比較的多く一つの処理で各々がどう絡み合っているのか視覚化する行為は、redux-saga を初めて触った身としてはアプリケーションの設計およびライブラリの理解にかなり貢献した。
  2. ビジネスロジックが集中し、それが視覚化されたことでワイヤーフレームとの仕様のすり合わせが比較的容易だった。

シーケンス図の作成は、サーバサイドの言語で設計に使うものだとずっと思っていましたが、saga task をシーケンス図で視覚化するということが思ったよりフィットしたので、もしフロントエンドの仕様やドキュメントの共有で上手くいっていない、黒魔術化してしてしまっている事案があったら、いったんUMLなどを使用して図として視覚化してみることをオススメします。

どこまで作り込むか、そもそも図式を書いている暇はあるのか?など日々フロントエンドで戦っているエンジニアさんはなかなか難しい問題も往々にしてあるかもしれませんが、もしこの記事を見て興味がでたり、ドキュメントの作成に困っている方の手助けになれれば幸甚です。

それでは!

 

 

  • フロントエンド
  • 開発プロセス

シェア

関連する記事

関連する求人