はじめに
こんにちは。Uniposではフロントエンドのリクエスト機構に関して試行錯誤を繰り返してきたので、その経緯について書いていきたいと思います。
ElmでのHTTP通信
まず大前提としてElmではHTTP通信用のパッケージとしてelm/httpが用意されているので基本的にこれを使います。詳細は割愛しますが、URLとレスポンスを受け取った際に発行されるMsgを指定しリクエストを送信します。
type Msg
= GotText (Result Http.Error String)
getText : Cmd Msg
getText =
Http.get
{ url = "https://hoge.com"
, expect = Http.expectString GotText
}
リクエストヘッダーを設定する関数も用意されているので認証情報を付加したリクエストを叩くこと自体は可能ですが、単純にこのパッケージを利用するだけではUniposのプロダクトで利用する上で以下のような課題がありました。
- 認証情報を必要に応じてリフレッシュする機構
- 認証情報のリフレッシュが必要になった場合、リフレッシュ中は新しいリクエストを待機させておき、リフレッシュ後に改めてリクエストを送信する必要があります。しかしリクエストは任意のモジュールから送信される可能性があるため、Mainアプリ全体のリクエストを最上位のモジュールで管理しなければならない。
そこで考案されたものがHttpAdapterです。
HttpAdapterとは
Uniposのプロダクトには、メインのアプリケーション(以下、Mainアプリ)とサーバの間に立って認証情報等の「HTTPリクエストに必要な雑務」をする役割を担うものがあり、それをHttpAdapterと呼んでいます。このHttpAdapterもMainアプリと同様にElmで実装されていますが、画面を持たないのでPlatform.worker
で実装されています。
このようにHttpAdapterはMainアプリがただ希望のリクエストを叩いて結果を得るということだけに専念すれば良いアプリケーションロジックに集中できる状態を目指して実装されました。
メリット
- 認証トークンの更新やセッションタイムアウトロジックがHttpAdapterに集約されることで、Mainアプリではその部分を意識せずに実装できた。
デメリット
一方でMainアプリと独立したアプリケーションとしてHttpAdapterが存在することによって以下のような課題が存在した。
- MainアプリとHttpAdapterは
port
を経由してやりとりする必要があり、実装中・リリース後に発覚する実行時エラーによる生産性・品質の低下があった。(portの外側の実装はElmのコンパイルでエラーを検知できない) - HttpAdapterはボイラープレート的に実装するコード量が多く、リクエストを叩く箇所が1つ増えるだけでも実装工数が増えやすい。
- 具体的には、、、
- リクエスト用port×2(Mainアプリ + HttpAdapter)
- レスポンス用port×2(Mainアプリ + HttpAdapter)
- MainアプリとHttpAdapterを繋ぐ部分のTSコード
- 具体的には、、、
- portは他の関数とは違い「1つのElmアプリケーションに対してユニークである」という特性からバグを生むことが少なくない。
- port名も冗長になっていた(ex.
getHogeForFuga
)
- port名も冗長になっていた(ex.
リクエスト機構β(上記の課題を踏まえた別のアーキテクチャ)
Uniposのプロダクトは複数のElmアプリケーションから構成されており、HttpAdapterとは異なるリクエスト機構で実装されたアプリケーションが存在する(社内ではこのリクエスト機構に名前がなかったので、勝手にリクエスト機構βと呼ばせていただきます)。
リクエスト機構βでは、HttpAdapterの課題を「Mainアプリとリクエスト機構を切り離すべきではなかった」と解釈しHttpAdapterを使わずに実装されました。
このリクエスト機構βでは、バックグラウンドで認証トークンを自動で更新し続けることで認証トークンが常に最新に保たれるようにすることで、HttpAdapterで目指していた「Mainアプリがアプリケーションロジックに集中できる状態」を作りました。
メリット
- HttpAdapter同様にMainアプリはアプリケーションロジックに集中できた。
- portを経由する必要がなくなりリクエストを叩く時点でレスポンスを受け取るMsgを指定できるので、コードを追いやすくなった。
デメリット
- 認証情報とリクエストの同期が取れないので、構造的に堅牢ではない。
- HttpAdapterには認証に失敗したリクエストを認証トークン更新後に再試行するロジックが組み込まれているが、このリクエスト機構βではそのようなロジックを組み込めないため「認証に失敗したらn秒後に再試行する」という処理になってしまっている。この処理ではn秒後に認証トークンが更新済みである保証がないことに加えて、オーバーヘッドが大きく体験的にも良いとは言えないことが課題として残ってしまった。
新HttpAdapter
HttpAdapterとリクエスト機構βから得られた学びを踏まえて誕生したのが新HttpAdapterになります。
前述の通りリクエスト機構βでは、「HttpAdapterのようにMainアプリとリクエスト機構を切り離すべきではなかった」と解釈したのに対して、新HttpAdapterでは**「portの使い方が上手くなかっただけだ」**と解釈しました。
従来のHttpAdapterはリクエストを叩くユースケースごとにportを定義する必要がありましたが、新HttpAdapterでは、Mainアプリ側で使うportをリクエスト用・レスポンス用の計2つに集約しました。このportを集約した状態で単純にportを使いまわすと、関係のないレポンスに対してもSub msg
が発行されてしまうので、ユースケースごとにIDを定義してリクエストを送信するようにしています。
また、従来のHttpAdapterはMainアプリとは別のアプリとして実装されていたが、新HttpAdapterはMainアプリ内にその機構が含まれるアーキテクチャになっています。
-- 従来のHttpAdapter
port getText : () -> Cmd msg
port gotText : (String -> msg) -> Sub msg
-- 新HttpAdapter
-- リクエストを送信するモジュールでは、下記2つのportをimportして使用する。
port sendRequest : Params -> Cmd msg
port returnResponse_ : (Result -> msg) -> Sub msg
-- これら2つのportはHttpAdapterモジュール内のみで使用される。上記2つのportと対になるもの。
port sendRequest_ : (RequestParams -> msg) -> Sub msg
port returnResponse : Result -> Cmd msg
改善点
新HttpAdapterでは従来のHttpAdapterの課題が以下のように改善することができました。
- port由来の実行時エラーによる生産性・品質の低下 → **portを定義する回数が激減した(というかほぼ無くなった)**ことで改善。
- ボイラープレートが多く実装工数が増えやすい → portの定義が不要かつ今までボイラープレートを書いてた部分はIDを定義するだけで良くなった。
課題・今後の展望
これまでリクエスト機構に関して多くの改善が行われてきた結果として新HttpAdapterという形を見つけることができたのですが、従来のHttpAdapterやリクエスト機構βの実装は今も残っており複数のリクエスト機構が混在してしまっています。それによってリクエスト機構の学習コストが高くなってしまったり、古いリクエスト機構に合わせて無駄な実装が必要になってしまっているので、現時点での最適解である新HttpAdapterに統一したいなーと思っています。(僕自身、今は開発から離れてしまっているので他力本願ですが、、、)