👇

on○○で送出するMsgはユーザーのアクションを表象するべき

2023/09/20に公開

Elmで、onClickが送出するMsgがどのようなデータを保持していればいいか悩んでいた。特に、バリデーション済みのデータをMsgのコンストラクタの中に保持するべきか?少し悩んだ。結論、on○○で送出するMsgはユーザーがシステムにどういう影響を与えるのか、そのアクション名であるべきだ。別の言い方でいうと、システムへ何をするか、Whatを送出する。そこに至るまでの思考の過程を残しておく。

具体例で考える

今回の考えるきっかけになったシステムを最小限で説明する。

システムの概要

今回作るフロントエンドに対応する、バックエンドの説明をさせてほしい。

  • 仮登録をする
    • 仮登録には名前が必要で、名前には特定の条件がある
    • 仮登録で、登録トークンが払い出される
  • 登録をする
    • 登録トークンを登録用endpointにPOSTする

このようなシステムを、はじめから最後まで通して叩いてみるというフロントエンドを作成しようとしていた。バックエンドが大まかにできているか、さっと確認するために作ったものだ。

この登録の流れを、一つの型として表現した。この際、Loadingなども「不正な状態が入らないように」それぞれ定義しておいた。

type RegisterStatus
	= Ready
	| PreeRegistering
	| PreRegistered | PreRegisterFailed
	| Registering
	| RegisterFailed | RegisterFailed

問題の箇所

問題になった、というか気になった箇所は、ユーザーからのアクション「仮登録」だ。
仮登録する際には、名前が必要なのであった。その際、バリデーションをして正しい名前だけを送信するようにしたい。そこで、二つの選択肢が私にはあった。

-- 案1
type Msg = ... | PreRegister ValidatedName

-- 案2
type Msg = ... | PreRegister

さて、どちらが良いのだろうか?案1でも特に実装上コストが余計にかかることは内容に見える。なぜなら、「仮登録」ボタンを押すときに、すでに名前はvalidatedかどうか判定しているはずだからである。だから、迷った。

バリデーション済みのデータを含むべきだろうか?update関数に入る直前のMsgのコンストラクタを、すでにバリデーション済みのものとして要求うすることで、update関数内で不正な値とやり取りをする必要がなくなる。スマートコンストラクターもしくはOpaque Types、ACLのような考え方である。

結局Msgというのは、送出されたあとにupdate関数の中で処理されることになるわけだけれど、view関数を書くのに集中しているときに、「後でupdate関数を実装するときに、ちゃんとvalidationやるかな?」って不安になったのだ。だから、先に入り口となるMsgを限定しておくことで、updateを実装しているときはvalidationについては考えなくて済むようにしたかった。

基本的に将来の自分を信用していないので、先に先に不安ごとは片付けてしまいたくなるのだ。

一つ一番最初に思い浮かぶのは、validation自体にコストがかかるということだ。どのようにvalidation関数を作るか?どこからライブラリを利用するか?など考慮対象がある。今回に関しては、validationのコストはupdateにあってもview関数で実装しても変わらない、と判断したので一旦これで良しとした。

問題を明確に整理してみると、ここで話題に上がっているのは、validationをupdate内で行うか、それともview関数内で行うかということだということに気づいた。


viewはユーザーとのインタラクションを定義する場所

viewはユーザーとのインタラクションを定義する場所である。そうであった。では、私は何に混乱していたのだろうか?それを探っていきたい。

まず、update関数は、ユーザーのアクションに対してシステムの変化を定義する。アクションの名前から、変化を記述するのだ。updateは、あくまでシステムの状態への変更メッセージを受け取るのみだ。

一つ懸念がある。私はなるべくmodelの正しさ、「不正な状態が入り込まない」ということを意識して実装している。そうすると、modelから値を取得するのが難しくなることが多い。これはトレードオフなので受け入れてはいるが、update関数が肥大化しすぎてしまうという問題がある。

そうか!update関数が肥大化するというのが問題なら、解決策は他にある。シンプルに関数を分割してしまえば良いんだ。updateは何をしているところだったか?考えてみると、

  • modelから必要な値のみを取得する
  • modelの適切な場所を更新する
  • Cmdを送出する

これぐらいだろう。このような関数を適切に分割していけば、アクション側でバリデーションを担ってあげよう、なんて変な気遣いはしなくて済むかもしれない。
そうだ、update関数はTEAの中で大きな役割を担っている。ユーザーからのアクションに対して状態を変更するという、システムで一番複雑になってしまう部分についてを担当しているから、肥大化して当たり前だな。updateの分割の仕方がこれからの実装の大きな観点の一つになりそうだ。

そこで、私が混乱していた場所は?という話だ。私が混乱していたのは、
view関数の中でも値のバリデーションをすることがあるよね、ということだった。
たとえば、Inputに入力されたEmailを送信するという要件があったとする。自然な流れで、入力されたEmailが正しくなければ「送信」ボタンをdisabledにしておきたいということはありうるだろう。これを、事前条件validationと呼ぶことにしよう。

私が無意識に気にかけていたのは、この状態遷移validationと、状態遷移中のデータ加工を混同していたのだ。今までの実装経験で、たとえばEmailやNameなど、それ自体でvalidationが完結して、状態を変化させるときにそれらを組み合わせたりする要件があまりなかった。(あまり現実的ではないが、EmailとNameの例で言えばcreateMailなどができるだろうか。)

今までは、たまたま事前条件validationとデータ加工がほぼ同じだったので混同しまったというわけだ。これを明確に見分ける方法は、シンプルにシステムへのアクション以前か以後か、ということだ。これを意識することにしよう。

これはElmだけでなく、Reactなどでも参考できるような考え方な気がする。関数の分け方など、ちょっと意識してみよう。

今回の学び

  • update関数を、model取得・データ加工・Cmd送信などに分割して考える
  • 事前条件validationとデータ加工を判別する

Discussion