Chapter 07

実例:単体テストとリファクタリング #3 〜仕様追加〜

とっくり
とっくり
2022.08.08に更新

リファクタリング=仕様変更フラグ!?

前回、いい感じでリファクタリングできましたが、ここでもっともな指摘が入ります。

さらに偉い人からも。

これに対処していきましょう。

方針:State.valueを変更すればいい

実は、リファクタリングの一番最初に作った、StateからDOMに書き戻す関数にこっそりこのための仕掛けを入れてました。

validator.js
/**
 * StateオブジェクトからDOMに書き戻す
 * @param {State} state Stateオブジェクト
 * @param {jQuery} ipt input要素
 * @param {jQuery} helper helper要素
 */
function fromState(state, ipt, helper) {

  ///// ここ! 実はstate.valueもinputに書き戻してました! /////
  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);
  }
}

予定調和と思われるかもしれませんが、実際、

DOMからState ---> Stateを変形 ---> Stateの内容をDOMに書き戻す

という流れはReduxなどでも一般的なもので、それほど変なコードではないと思います。

これがあるので、全角→半角変換やハイフン無し→ハイフン有りといった正規化をStateオブジェクトのvalueに加えるだけで、今回の仕様追加は解決できます。

全角→半角の正規化

仕様を追加するのですから、まずはテストコードを追加しましょう。TDDは最高だ!

validator.test.js
test("validateZip normalize to ascii", () => {
  const result = Validator.validateZip({
    value: "012ー3456", // 全角数字、全角ハイフン
    valid: true,
    message: ""
  });
  expect(result).toStrictEqual({
    value: "012-3456", // 半角に正規化される
    valid: true,
    message: ""
  })
});

もちろんテストは失敗して、ここからコーディング開始です。

JavaScriptで全角を半角に変換するにはーとググるとどうやればいいか見つかるので、それを利用しますが、ここでもStateオブジェクトが引数で、Stateオブジェクトを返す純粋関数を作ります。

validator.js
Validator.normalizeToAscii = function(state) {
  function toAscii(str) {
    return str.replace(/[---@.]/g, function(s) {
      return String.fromCharCode(s.charCodeAt(0) - 65248);
    }).replace(/[ー−―‐]/, "-"); // いろんな横棒を半角ハイフンに正規化
  }
  return {
    ...state,
    value: toAscii(state.value)
  };
}

すると、validateZipはこうなります。

validator.js
Validator.validateZip = function(state) {
  const state1 = Validator.normalizeToAscii(state);
  const state2 = Validator.checkEmpty(state1, "郵便番号を入力してください");
  return Validator.checkPattern(state2, /^\d{3}-\d{4}$/, "000-0000の形式で入力してください");
}

normalizeToAsciiの返り値をcheckEmptyに渡し、その返り値をcheckPatternに…というように数珠つなぎでStateオブジェクトを変形していくスタイルです。なんだか気持ちいいですね。これでテストも合格、デグレードもありません。最高だ!

ハイフン有り無しの正規化

テストを作って…

validator.test.js
test("validateZip normalize ZIP code format", () => {
  const result = Validator.validateZip({
    value: "0001111", // 全角、ハイフンなし
    valid: true,
    message: ""
  });
  expect(result).toStrictEqual({
    value: "000-1111", // 半角、ハイフンあり
    valid: true,
    message: ""
  })
});

ハイフン無しを有りに正規化する関数も同様に作って…

validator.js
Validator.normalizeZipFormat = function(state) {
  function normalize(str) {
    const m = str.match(/^(\d{3})(\d{4})$/)
    if (m) {
      return m[1] + "-" + m[2];
    } else {
      return str;
    }
  }
  return {
    ...state,
    value: normalize(state.value)
  };
}

validateZipで数珠つなぎをさらに伸ばします。

validator.js
Validator.validateZip = function(state) {
  const state1 = Validator.normalizeToAscii(state);
  const state2 = Validator.normalizeZipFormat(state1);
  const state3 = Validator.checkEmpty(state2, "郵便番号を入力してください");
  return Validator.checkPattern(state3, /^\d{3}-\d{4}$/, "000-0000の形式で入力してください");
}

結構な機能追加をしていますが、単体テストがずっと走ってデグレードがないことを保証してくれているので、ブラウザで動作チェックしなくてもちゃんと動くであろうことが予想できますね。純粋関数+単体テストは最高だ!

大文字小文字の正規化

全く同様です。テストを作ってから機能を追加することを忘れないでください。

ここまでの途中経過

ここまでの途中経過のコードです。

https://github.com/tockri/not-scary-fp/tree/master/ex-form-validation/refactor-3

実際動くコードはこちらです。

次回はいよいよもっと関数型っぽくしていきます。

純粋関数は変更に強い!?

この記事を書くための恣意的な仕様変更だからこんなにすんなりいくんだろう、と思われるでしょう。まあ、その通りなんですが。

しかし、純粋関数で書こう!と強い気持ちで書いていくと気づくのですが、純粋関数は挙動がシンプルなので、関数名を考えやすいです。また「一つの関数が一つのことしかしない」という理想的な状態になりやすいので、結果的に再利用性が高く変更に強いコードになりやすいです(個人の感想です)。

関数が副作用を含んでいると、「一つの関数が複数のことをする」ため、複数の機能のうち片方が邪魔になっても切り離せないとか、仕様変更に弱くなります。