[React]いかにドメイン知識(ビジネス概念)をコンポーネントに落とし込むべきか???
導入
フロントエンド開発者でドメイン駆動設計、オブジェクト指向プログラミングやアトミックデザインなど知識がこんがらがっている方へ。
今や、React.js/Next.js、Vue.js/Nuxt.js、Angular.jsがフロントエンドにおいて技術採用されることが多いですね。
世界を見渡せば、React.js/Next.jsが主流で、日本はVue.js/Nuxt.jsもまだ採用がある方ではあります。
そのフロントエンドにおいて、コンポーネント設計というのが当たり前に聞かれるようになったのではないでしょうか?
フロントエンドの設計ではアトミックデザインというのがありますが、オブジェクト指向プログラミングやドメイン駆動設計とどう組み合わせて開発したらよいか分からないと悩んでいる方も多いはずです。
ReactのuseContextが使えるようになってから、上記の設計方法をコンポーネントの実装にだいぶ落とし込みやすくなったと思います。
以前はReduxを使うということも選択肢としてはありましたが、最近、Reduxはあまり使われていないように思います。
この記事では、アトミックデザイン、オブジェクト指向プログラミング、ドメイン駆動設計などの原則を取り入れながら、コンポーネントの実装方法について提案いたします。
ドメイン知識(ビジネス概念)をコンポーネントで表現する重要性
オブジェクト指向プログラミング(OOP)やアトミックデザイン(atomic design)を取り入れて、プロジェクトのドメイン知識(ビジネス概念)を、オブジェクトまたはコンポーネントとして、いかに表現するべきかは大変重要な課題です。
単一責任の原則、高凝集、疎結合(SOLIDの原則も含む)も達成して、内部品質も高めながら、プロジェクトのコンポーネントを設計・プログラミングしていかなければなりません。
でなければ、プロジェクトの開発が進むにつれて、いずれ神クラスという何千、何万行という、恐るべき魔物が何匹も誕生してしまいます。コードの読解だけで日が暮れてしまう、複雑になったコードの影響範囲を把握するためにレビューで1日が終わってしまう、なんて生産性低下への道へまっしぐら。。。
そんな神クラスがあっちこっちに誕生してしまい、炎上プロジェクトになってしまうというのもよくある話。そんな現場を私も経験してきました。なので内部品質には結構うるさい方かもしれません
ドメイン知識(ビジネス概念)の電話番号でさえ侮れない
例として、電話番号というドメイン知識(ビジネス概念)を挙げます。
電話番号といっても、日本の電話番号、アメリカの電話番号、中国の電話番号など世界の国々によって電話番号の形式は全く異なります。さらにこれを固定電話、ナビダイヤル、都道府県、州、携帯電話などに分解するとそのパターンは膨大です。
グローバル展開を目指しているプロジェクトならば、なおさら電話番号を一つ取ってみても侮ってはいけないのです。日本とアメリカの電話番号でも形式は全く異なるのですから。
ではどのように異なるのでしょうか?実際に見てみましょう。
日本とアメリカの電話番号の主な違いは、番号の形式と長さです。
形式の違い:
日本の電話番号は、通常、0から始まり、市外局番(都道府県コード)と市内局番(市町村コード)を含む。たとえば、東京の電話番号は「03-XXXX-XXXX」という形式。ここで、03は東京の市外局番。
アメリカの電話番号は、一般的に3つの部分から構成される。エリアコード(市外局番)、中間の3桁の番号、そして最後の4桁の番号。たとえば、ニューヨーク市の電話番号は「(212) XXX-XXXX」という形式。ここで、212はエリアコード。
長さの違い:
日本の電話番号は、市外局番を含めて10桁。市外局番がなければ8桁。市外局番は2桁、市内局番は4桁、残りの4桁は個別の電話番号。
アメリカの電話番号は、エリアコードを含めて通常10桁。エリアコードは3桁、中間の3桁の番号、残りの4桁の番号が含まれる。
このように、日本とアメリカの電話番号は形式と長さの面で全く異なることがお分かりいただけたのではないでしょうか。
電話番号の違いからだけでも国の判定まで行えます。それによってプロジェクトが提供するサービス内容も全く変わるかもしれません。
電話番号というドメイン知識(ビジネス概念)さえも簡単には侮れないことが分かります。
電話番号を具体的にコンポーネントで表現する
電話番号とひとえにいっても、様々な要素からそのコンポーネントは構成されます。
ここでは、5つのコンポーネントに分割してアトミックデザインも取り入れて、以下のように設計・プログラミングを行います。
PhoneNumberValidity(電話番号の有効性を管理するコンテキスト)
PhoneNumberValidityManager(電話番号の有効性を管理するプロバイダ)
PhoneNumberField(電話番号フィールド全体を管理する親コンポーネント)
PhoneNumberInput(電話番号の入力を管理するコンポーネント)
PhoneNumberError(電話番号のエラーメッセージを表示するコンポーネント)
PhoneNumberValidity.tsx
import createContext from 'react';
export const NO_ERROR = { hasError: false };
export const ERROR = { hasError: true };
interface ValidationStatus {
hasError: boolean;
}
const PhoneNumberValidity = createContext<ValidationStatus>(NO_ERROR);
export default PhoneNumberValidity;
PhoneNumberValidityManager.tsx
import { React,
useState
} from 'react';
import { PhoneNumberValidity,
ValidationStatus,
NO_ERROR
} from './PhoneNumberValidity';
export const PhoneNumberValidityManager: React.FC = ({ children }) => {
const [validationStatus, setValidationStatus] = useState<ValidationStatus>(NO_ERROR);
return (
<PhoneNumberValidity.Provider value={{ ...validationStatus, setValidationStatus }}>
{children}
</PhoneNumberValidity.Provider>
);
};
PhoneNumberField.tsx
import React from 'react';
import PhoneNumberInput from './PhoneNumberInput';
import PhoneNumberError from './PhoneNumberError';
import PhoneNumberValidityManager from './PhoneNumberValidityManager';
const PhoneNumberField: React.FC = () => {
return (
<PhoneNumberValidityManager>
<div>
<h3>Phone Number Field</h3>
<PhoneNumberInput />
<PhoneNumberError />
</div>
</PhoneNumberValidityManager>
);
};
export default PhoneNumberField;
PhoneNumberInput.tsx
import { React,
useState,
useContext
} from 'react';
import PhoneNumberValidity from './PhoneNumberValidity';
import { PhoneNumberValidity,
ValidationStatus,
NO_ERROR,
ERROR
} from './PhoneNumberValidity';
const NO_NUMBER = '';
const PhoneNumberInput: React.FC = () => {
const [phoneNumber, setPhoneNumber] = useState(NO_NUMBER);
const validationStatus: ValidationStatus = useContext(PhoneNumberValidity);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newNumber = e.target.value;
setPhoneNumber(newNumber);
if (/^[0-9]{10,11}$/.test(newNumber)) {
validationStatus = NO_ERROR;
} else {
validationStatus = ERROR;
}
};
return (
<div>
<label>Phone Number</label>
<input type="text" value={phoneNumber} onChange={handleInputChange} />
</div>
);
};
export default PhoneNumberInput;
PhoneNumberError.tsx
import { React,
useContext
} from 'react';
import PhoneNumberValidity from './PhoneNumberValidity';
const PhoneNumberError: React.FC = () => {
const validationStatus = useContext(PhoneNumberValidity);
return (
<div>
{validationStatus.hasError && <span>Invalid phone number</span>}
</div>
);
};
export default PhoneNumberError;
この設計により、PhoneNumberValidityManager が PhoneNumberValidity コンテキストを提供し、その子コンポーネントである PhoneNumberInput と PhoneNumberError がこのコンテキストを利用します。
これにより、電話番号の有効性に関する状態が共有され、適切に管理されるようになりました。
電話番号入力部分はPhoneNumberInput 、電話番号のエラーメッセージはPhoneNumberError のコンポーネントでそれぞれ適切に管理することで、今後アメリカなどの電話番号のビジネス概念を表現したいときやエラーメッセージを変更したいときなどにも、すぐさま開発対応できる状態を整えることができるようになりました。
電話番号の番号自体は、より高位な入力フォームなどのコンポーネントでuseContextを使ってStateの管理する方がよいかもしれません。
いずれにしても、電話番号というビジネス概念がPhoneNumberFieldというコンポーネントをひとまとめにして分かりやすい形で表現することができるようになり、開発の見通しもかなり良くなったことに違いはありません。
まとめ
ドメイン知識(ビジネス概念)をオブジェクト指向プログラミングの要領で捉えなおして、アトミックデザインも取り入れて、コンポーネントを設計することがいかに有用かが伝わったのではないでしょうか?
電話番号のコンポーネントの設計とプログラミングですが、単純なように見えて、以下の点に留意しながら考案しました。
Stateのスコープの最小化、オブジェクト指向プログラミング、アトミックデザイン、SOLIDの原則、NULL問題の解決、生焼けオブジェクト防止、高凝集、疎結合など。
意外と様々なことを念頭に入れて設計、プログラミングしています。
シンプル イズ ザ ベストという言葉がありますがシンプルなものほど奥深い、そのようなことを思います。
プロジェクトは常に最善を目指して開発を行うべきです。なのでこの記事が絶対ではありません。
より良いコードとは何か日々研究、開発しながら、常に改善していくべきものだと思います。
この記事が皆さんの日々の開発でお役に立てれば幸いです。