Chapter 04

実例:フォームの入力値バリデーション

とっくり
とっくり
2022.06.14に更新

さて、文章ばかり書いていてもなんですので、早速プログラムを書いていきます。最初の例はJavaScriptでやってみましょう。「お客様情報」を登録するフォームです。

関数型でないスタイルでべた書き

まずフォーム仕様の確認もかねてべた書きのコードから始めます。「Result」ボタンクリックで実際の挙動も見られます。

見栄えを良くするためにMaterializeを、コードを短くするためにjQueryを使用していますが、やってることは以下の通りです。

  • validateName : 氏名が空ならエラー
  • validateZip : 郵便番号が空ならエラー、000-0000のフォーマットに一致しなければエラー
  • validateAddress : 住所が空ならエラー
  • validateMail : メールアドレスが空ならエラー、xxx@yyy.zzzのフォーマットに一致しなければエラー

多少繰り返しが目に付きますが、まあこんなもんではないでしょうか。

このコードの問題点

しかし、このコードではどの関数も単体テストが簡単にはできません。単体テストしようとおもったらモックのHTMLやHeadless Chromeなど、かなり大掛かりな仕掛けが必要になってしまいます。

面倒なので少し変更するたびにいちいちブラウザを起動してポチポチ手作業で動作確認する…ということになり、残業代がたくさん貰えてウレシイデス!

純粋関数を作ってみよう

なぜこのコードでテストができないかというと、jQueryでDOMアクセスしている部分、つまり副作用が関数内に散りばめられているからです。

関数型プログラミングの常套手段として、I/Oが必要な箇所を処理の最初と最後に固めてしまう、というものがあります。

準備:純粋関数で扱うためのオブジェクト

次のような型"State"を導入します。(ここだけ説明のためにTypeScriptの文法)

interface State {
  // inputの値
  value: string
  // 値が妥当かどうか
  valid: boolean
  // 表示メッセージ
  message: string
}

これから、以下のような純粋関数を作っていきます。

  1. 引数でStateを受け取る
  2. 与えられたStateのvalueに対してチェックする
  3. 問題なければ valid = true であるような新しいStateオブジェクトを返す
  4. 問題があれば valid = false で message にエラーメッセージを持つ新しいStateオブジェクトを返す

準備:副作用を最初と最後に寄せる

純粋関数の中ではDOMアクセスはしません。最初にDOMからStateオブジェクトを作って、それを引数に渡していきます。

/**
 * DOMからStateオブジェクトをつくる
 * @param {jQuery} ipt input要素
 * @return {State}
 */
function toState(ipt) {
  return {
    value: ipt.val(),
    valid: true,
    message: '',
  }
}

最後にStateオブジェクトをDOMに書き戻すコードはこうなります。

/**
 * StateオブジェクトからDOMに書き戻す
 * @param {State} state Stateオブジェクト
 * @param {jQuery} ipt input要素
 * @param {jQuery} helper helper要素
 */
function fromState(state, ipt, helper) {
  ipt.val(state.value)
  if (state.valid === true) {
    ipt.removeClass('invalid')
    ipt.addClass('valid')
  } else if (state.valid === false) {
    ipt.removeClass('valid')
    ipt.addClass('invalid')
    helper.attr('data-error', state.message)
  }
}

準備完了!ロジック部分を純粋関数にする

そしてソースコード全体はこのようになります。フォームの仕様は変わっていません。

まだ最初のコードと比べて、それほど大した差はありませんね。ただ、validateName()などの関数からjQueryが消え、純粋関数になっています。

/**
 * 名前のバリデーション(純粋関数)
 * @param {State} state
 * @return {State} 結果のstate
 */
function validateName(state) {
  if (state.value !== '') {
    // もとのオブジェクトを変更せずコピーして新しいオブジェクトを返す。
    // 引数を変更したら純粋関数ではなくなるので。
    return {
      ...state,
      valid: true,
      message: '',
    }
  } else {
    return {
      ...state,
      valid: false,
      message: '名前を入力してください',
    }
  }
}

コメントにある通り、純粋関数では引数で与えられたオブジェクトに変更を加えるのも禁止で、処理の結果は返り値だけに現れなければいけません。そのため、Stateオブジェクトの変形を行う際は、元のオブジェクトのコピーを変更して新しいオブジェクトを返します。

このあたりも関数型プログラミングのお約束というか、常套手段です。

なお、IE11ではこの {...state, } という文法(スプレッド構文)がサポートされてないので、IE11をサポートする場合は

return Object.assign(state, {
  valid: true,
  message: '',
})

のように書きます😢

2022年追記:もうIE11はサポートしなくてよくなりましたね!