通信が絡むUIを仕様書で定義するときのパターン
これらを未然に防ぐために、通信に関する状態を正しく理解し、仕様書の段階で実装にブレが生じない定義を行いたい。
用語 | 意味 |
---|---|
通信 | HTTP通信など |
理解を開始するためにはまずは「分ける」ところから始めます。
通信の状態は次の4つの状態があります。
通信状態 | 意味 |
---|---|
待機 (Standby) | 通信の開始前の状態。通信終了から次の通信開始までの状態。 |
実行中 (In Progress) | 通信中の状態。 |
完了 (Complete) | サーバーからの応答を正常に受け付けた状態 |
中断 (Abort) | タイムアウト。ユーザーによるキャンセル |
※ 「中断」はクライアントに対してサーバーからの応答がない状態です。
すくなくとも3種類のUIを利用することで通信の状態を伝えられます。
UIの状態 | 意味 |
---|---|
待機 | 初期表示と同義。ユーザーからのイベントを受け付ける表示状態 |
実行中 | 通信が処理中の表示状態 |
通知 | 通信が完了、もしくは中断した場合にユーザーに通知する表示状態 |
待機表示の無効化
disabled
にするテキストの変更
アニメーションによる読み込み表示
Buttonをクリックしたときに、通信が発生し、通信中はLoaderが表示され、通信が完了した場合はToastを表示、中断した場合はDialogを表示する
を仕様書で表現するための例を示します。
UI | 通信:待機 | 通信:実行中 | 通信:完了 | 通信:中断 |
---|---|---|---|---|
Button | 表示 | 表示 | 表示 | 表示 |
Loader | 非表示 | 表示 | 非表示 | 非表示 |
Toast | 非表示 | 非表示 | 表示 | 非表示 |
Dialog | 非表示 | 非表示 | 非表示 | 表示 |
:::message この表の意図 通信に関連するUIの表示状態を通信のシーケンスに対応して書くことで、表示するUIと表示しないUIの抜け漏れを防ぐ。 :::
:::message この項目の意図 表示状態のマトリックスは登場自分物の整理に利用するが、本節は登場人物がどのような振る舞いをするのかを詳細に記述する。 :::
Buttonの仕様
通信の状態 | UIの状態 |
---|---|
待機 | クリックできる状態 |
実行中 | クリックできない状態 |
完了 | クリックできない状態。Toastを閉じることによってクリック可能となる。 |
中断 | クリックできない状態。Dialogを閉じることによってクリック可能となる。 |
Loaderの仕様
Toastの仕様
Button
のdisabled
が解除される。Dialogの仕様
Button
のdisabled
が解除される。前節までに示した内容をフローチャートに書き起こすと次のようになります。
このチェック項目は仕様書のレビュー時にでも利用してください。
:::message 仕様書を書くときは「明文化」することが重要です。通信の状態の用語を正確に使うことにより、読者が複数の意味で受け取ることができないように防ぐべきです。 :::
自分が実際に経験したパターンを紹介します。随時追加していきます。
単純な実装例は次の通り(JavaScript)。
try { const res = await fetch("..."); if (res.ok) { return true; } return false; } catch (error) { return false; }
問題となるのは、通信の「完了」と「中断」、処理全体の「失敗」が混ざり合っている点です。
try { const res = await fetch("..."); if (res.ok) { return true; } // 通信の「完了」の結果が成功 return false; // 通信の「完了」の結果が失敗 } catch (error) { return false; // 通信の「中断」か、処理の失敗どっちか }
これがなぜ"問題"なのかというと、この通信の処理全体をどのUIに対してマッピングするのか、正しくできない点にあります。例えば、「ユーザーに対して、HTTP通信の結果が500を返しているので、サーバー側のエラーです」と判別する方法が握りつぶされています。
上記のコードを書き換え、意図が分解できる状態にするには次のように書き換えます。
// ※ 関数に切り出したり細かいことは本当はあるけどエッセンスだけ伝われば良い try { const res = await fetch("..."); if (isSuccessResponse(res)) { // この処理が期待する「成功」のレスポンスに対する処理 } else { // この処理が期待する「失敗」のレスポンスに対する処理 } } catch (error) { if (error instanceof TimeoutError) { // 通信処理がタイムアウト(中断)した場合のエラー } throw new Error(error); // 通信以外の処理に失敗したことをThrowする }
処理の流れを明示的に分類し、「わかった上で」エラーを握りつぶすのであればこういった処理は必要ありません。しかしながら、わかっていない状態でなんとなくエラーを握りつぶしていると仕様変更を受けたときに痛い目を見ます。
:::message このエラーを握りつぶすパターンはBFFをする場合にありがちなパターンです。フロントエンド用のAPIを集約するサーバーの中で、特定のマイクロサービスが死んだ場合に代替値を利用することがあります。これを行うと、「ユーザーに対してサーバーが死んでいるから〜」と判別する方法を失います。 :::
他に思いつくパターンがあれば追記します。コメントでも歓迎しております!
通信に関連したUIの仕様はある程度規格化することができます。 通信の状態とそれに対応するUIを用意しておけば仕様書を管理するコストが一気に減ることになり、またユーザビリティの向上にも繋がることと思います。