見出し画像

React と Value Object で凝集度を高める

こんにちは。 Showcase Gig の金子です。

テイクアウト向けモバイルオーダーサービス「O:der ToGo(オーダートゥーゴー)」のフロントエンドエンジニアをしています。

本記事では、React に Value Object を適用した事例と、実装で得た気付きを共有します。

背景

React でコーディングを進めていく際に、多くのドメインロジックをフロントエンドで実装していました。

多くのドメインロジックを含んでいる例を、 redux-ddd-exampleより引用します。

items を算出するために、コンポーネント内にドメインロジックが散在しています。

function TodoItems (props) {
  const { todos, updateTodo, deleteTodo } = props
  const items = todos.map(item , i) => {
    const { done, show_flag, created_at } = item
    const current = new Date().getTime()
    const created = created_at.getTime()
    const day_over = (current - created) / (1000 * 60 * 60 * 24) >= 1
    if (done || !show_flag || day_over) return null
    const _props = { key: i, index: i, item, updateTodo, deleteTodo }
    return <TodoItem { ..._props } />
  })
  return (
    <div className="todo-items">
      <ul className="list-group">
        { items }
      </ul>
    </div>
  )
}

事例

「カード番号のテキストフィールド」コンポーネントを作成した際のコードを例に説明します。

主な要件として、以下の 3 つがありました。

  1. 特定の桁数ごとに空白を入れる

  2. カード番号よりブランドを判別する

  3. 14 桁〜16 桁の数字

上記の要件を順に実装してみます。

完成イメージを以下に示します。

1. 特定の桁数ごとに空白を入れる

例として、4 桁ごとに半角スペースを入れるコードを考えます。 完成イメージは以下のようになります。

withWhiteSpace で 4 桁毎に空白を追加し、removeWhiteSpace で空白を除去することで、ステートとしては空白を除去した値を持ち、表示する際に空白を付与するようにします。

const removeWhiteSpace = (cardNumber: string): string => {
  return cardNumber.replace(/ /g, "");
};

const withWhiteSpace = (cardNumber: string): string => {
  return cardNumber.match(/\d{1,4}/g)?.join(" ") || "";
};

const CardNumberTextField: React.VFC = () => {
  const [value, setValue] = useState("");

  const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    setValue(removeWhiteSpace(e.target.value));
  };

  return (
    <div>
      <input value={withWhiteSpace(value)} onChange={handleChange} />
    </div>
  );
};

2. カード番号よりブランドを判別する

例として、VISA と Amex を判定します。 完成イメージは以下のようになります。

isVisa と isAmex より、 4 から始まる場合は VISA、34 または 37 から始まる場合は AMEX と判定します。

const isVisa = (cardNumber: string): boolean => /^4/.test(cardNumber);

const isAmex = (cardNumber: string): boolean => /^3[47]/.test(cardNumber);

const CardNumberTextField: React.VFC = () => {
  const [value, setValue] = useState("");

  const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    setValue(e.target.value);
  };

  return (
    <div>
      {isVisa(value) && "VISA"}
      {isAmex(value) && "AMEX"}
      <input value={value} onChange={handleChange} />
    </div>
  );
};

3. 14 桁〜16 桁の数字

例として、14 桁未満または 17 桁以上 の文字列を入力したとき、エラーメッセージを表示するコードを考えます。

isValid より、14 桁〜16 桁の数字 か否かを判定し、エラーメッセージを表示します。

const isValid = (cardNumber: string): boolean => {
  const { length } = cardNumber;
  return 14 <= length && length <= 16;
};

const CardNumberTextField: React.VFC = () => {
  const [value, setValue] = useState("");

  const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    setValue(e.target.value);
  };

  return (
    <div>
      <input value={value} onChange={handleChange} />
      <div>{!isValid(value) && "14 ~ 16桁で入力してください"}</div>
    </div>
  );
};

まとめると

1〜3 をまとめると、以下のようになります。

const removeWhiteSpace = (cardNumber: string): string => {
  return cardNumber.replace(/ /g, "");
};

const withWhiteSpace = (cardNumber: string): string => {
  return cardNumber.match(/\d{1,4}/g)?.join(" ") || "";
};

const isVisa = (cardNumber: string): boolean => /^4/.test(cardNumber);

const isAmex = (cardNumber: string): boolean => /^3[47]/.test(cardNumber);

const isValid = (cardNumber: string): boolean => {
  const { length } = cardNumber;
  return 14 <= length && length <= 16;
};

const CardNumberTextField: React.VFC = () => {
  const [value, setValue] = useState("");

  const handleChange = (e) => {
    setValue(removeWhiteSpace(e.target.value));
  };

  return (
    <div>
      {isVisa(value) && "VISA"}
      {isAmex(value) && "AMEX"}
      <input value={withWhiteSpace(value)} onChange={handleChange} />
      <div>{!isValid(value) && "14 ~ 16桁で入力してください"}</div>
    </div>
  );
};

カード番号を引数とする関数がバラバラと作られていることがわかります。

そこで、プリミティブ型では表現力がないことを原因と考え、Value Object の適用を検討しました。

Value Object の適用

「カード番号のテキストフィールド」コンポーネントに Value Object を適用してみます。

カード番号の Value Object

まず、カード番号の Value Object を作成します。

export class CardNumber {
  readonly cardNumber: string;

  constructor(cardNumber: string) {
    this.cardNumber = this.removeWhiteSpace(cardNumber);
  }

  get isVisa(): boolean {
    return /^4/.test(this.cardNumber);
  }

  get isAmex(): boolean {
    return /^3[47]/.test(this.cardNumber);
  }

  get withWhiteSpace(): string {
    return this.cardNumber.match(/\d{1,4}/g)?.join(" ") || "";
  }

  get isValid(): boolean {
    const { length } = this.cardNumber;
    return 14 <= length && length <= 16;
  }

  private removeWhiteSpace(cardNumber: string): string {
    return cardNumber.replace(/ /g, "");
  }
}

コンストラクタで空白を除去し、空白なしのカード番号のみ存在するようにします。 そして、カード番号に関連する振る舞いを Value Object に凝集します。

コンポーネントから Value Object を利用する

次に、コンポーネントから Value Object を呼び出します。

import { CardNumber } from "./CardNumber";

const CardNumberTextField: React.VFC = () => {
  const [value, setValue] = useState(new CardNumber(""));

  const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    const newValue = new CardNumber(e.target.value);
    setValue(newValue);
  };

  return (
    <div className="App">
      {value.isVisa && "VISA"}
      {value.isAmex && "AMEX"}
      <input value={value.withWhiteSpace} onChange={handleChange} />
      <div>{!value.isValid && "14 ~ 16桁で入力してください"}</div>
    </div>
  );
};

まず、useState にカード番号インスタンスを設定します。クレカ判定 (isVisa) や 空白付与 (withWhiteSpace) 等の振る舞いは、Value Object メソッドを呼び出します。

また、値を更新する際は、新しいインスタンスを作成し、交換します。

まとめ

  • Value Object の恩恵を得られた

React に Value Object を適用することで、表現力が増し、不正な値は存在しなくなり、ロジックの散在を防げるようになりました。

  • React と Value Object の親和性

Value Object の 不変 かつ 交換可能 という性質が、React のステートの特性と相性が良いことに気付きました。

参考

  • redux-ddd-example

  • ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本 成瀬 允宣 (著)

この記事が気に入ったらサポートをしてみませんか?