はじめに
私は普段 React を使ってフロントエンドの開発をしています。
React でもっと良いコードが書けるようになりたいと思い、良いコードを書くためにどういうことを意識すれば良いかいろいろ調べている中で、結合度というものを知りました。
この結合度という指標を用いて React のコードのよさを評価することを考えてみました。
結合度
ソースコードの良し悪しをはかる指標として、結合度 というものがあります。
結合度はモジュール間の関係のよさ(依存性の程度)を示しています。
結合度は 7つのレベルで表され、結合度が低い、つまり疎結合であるほど 可読性・保守性 が高く、良いとされます。
モジュール間の関係のよさを示す指標
レベル | 説明 | 使い分け |
---|---|---|
内容結合(Content coupling) | あるモジュールの動作が別のモジュールの動作に依存する | 必ず避けるべき |
共通結合(Common coupling) | 複数のモジュールが同じグローバルデータにアクセス(変更)する | 可能な限り避けるべき |
外部結合(External coupling) | グローバルデータへのアクセスが、標準化されたインターフェースによって制御された状態。外部ツールや外部デバイスへの通信で発生する可能性がある。 | 可能な限り避けるべき |
制御結合(Control coupling) | あるモジュールが別のモジュールの動作を制御する呼び出しをする | 可能な限り避けるべき |
スタンプ結合(Stamp coupling) | あるモジュールが別のモジュールに構造体(Object)を渡す | 一部ケースで注意が必要 |
データ結合(Data coupling) | あるモジュールが別のモジュールにプリミティブなデータを渡す | 理想的 |
メッセージ結合(Message coupling) | あるモジュールが別のモジュールに引数のない呼び出しをする | 理想的 |
もちろん、すべての結合をメッセージ結合にすることはできませんが、制御結合より上のものは可能な限り避けるべきだと言われています。
コンポーネントの結合度評価
React のコンポーネントについても、上記の定義に照らし合わせて結合度を評価することができます。
React においてコンポーネントがグローバルデータにアクセスするようなコードを書くことはあまりないと思います。
ただ、親コンポーネントが子コンポーネントを呼び出す場合などに、密結合になってしまうことがあります。
関数などでは引数の型によって結合度を評価することができますが、React コンポーネントの場合には props の型によって同様に結合度を評価することができます。
例1:ボタンをもつモーダルのコンポーネント
コンポーネントの結合度を評価する例として、以下のようなモーダルのコンポーネントを考えてみます。
- このモーダルは1つまたは2つのボタンをもつ
- props でボタンの情報を渡す
- ボタンの属性として、以下を指定できる
- テキスト
- 背景色
- ボタン押下時に発火するイベント
愚直な実装として、2つめのボタンに関する props をオプショナルとして、指定された場合のみ2つめのボタンを表示するようにしてみます。
type ModalProps = {
isOpen: boolean;
firstButtonText: string;
secondButtonText?: string;
firstButtonBackgroundColor: string;
secondButtonBackgroundColor?: string;
onClickFirstButton: MouseEventHandler<HTMLButtonElement>;
onClickSecondButton?: MouseEventHandler<HTMLButtonElement>;
};
const Modal: React.FC<ModalProps> = ({
isOpen,
firstButtonText,
secondButtonText,
firstButtonBackgroundColor,
secondButtonBackgroundColor,
onClickFirstButton,
onClickSecondButton,
}) => {
return (
<div className={`${style['modal-default']} ${!isOpen && style['-hidden']}`}>
<div
className={`${style['button']} ${
style[`-color-${firstButtonBackgroundColor}`]
}`}>
<button onClick={onClickFirstButton} type={'button'}>
{firstButtonText}
</button>
</div>
{onClickSecondButton &&
onClickSecondButton &&
secondButtonBackgroundColor &&
secondButtonText && (
<div
className={`${style['button']} ${
secondButtonBackgroundColor &&
style[`-color-${secondButtonBackgroundColor}`]
}`}>
<button onClick={onClickSecondButton} type={'button'}>
{secondButtonText}
</button>
</div>
)}
</div>
);
};
export default Modal;
使うときは
const Index: NextPage = () => {
return (
<div className={style['modal']}>
<Modal
isOpen={isOpen}
firstButtonText={'OK'}
firstButtonBackgroundColor={'blue'}
onClickFirst={() => onClickOkButton()}
secondButtonText={'CANCEL'}
secondButtonBackgroundColor={'gray'}
onClickSecondButton={() => onClickCancelButton()}
/>
</div>
);
};
export default Index;
このような感じです。
このときの結合度ですが、 7つのレベルでいうと 制御結合 にあたります。
なぜなら、2つめのボタンに関する props の値によってモーダルの表示が変わるからです。
呼び出し側の secondButtonText
などによってこのモーダルの挙動を制御していると言えます。
このような場合は、一つのコンポーネントで2パターンのボタンの数に対応するのではなく、別々のコンポーネントを用意してしまったほうが結合度が下がります。
ボタンを1つもつモーダルコンポーネントとボタンを2つもつモーダルコンポーネントをそれぞれ用意し、呼び出し側で欲しいほうを使うといった形です。
例2:ボタンを2つもつモーダルのコンポーネント
ボタンを2つ持つモーダルコンポーネントはこのようになります。
type DoubleButtonModalProps = {
isOpen: boolean;
firstButtonText: string;
secondButtonText: string;
firstButtonBackgroundColor: string;
secondButtonBackgroundColor: string;
onClickFirstButton: MouseEventHandler<HTMLButtonElement>;
onClickSecondButton: MouseEventHandler<HTMLButtonElement>;
};
const DoubleButtonModal: React.FC<DoubleButtonModalProps> = ({
isOpen,
firstButtonText,
secondButtonText,
firstButtonBackgroundColor,
secondButtonBackgroundColor,
onClickFirstButton,
onClickSecondButton,
}) => {
return (
<div className={`${style['modal-default']} ${!isOpen && style['-hidden']}`}>
<div
className={`${style['button']} ${
style[`-color-${firstButtonBackgroundColor}`]
}`}>
<button onClick={onClickFirstButton} type={'button'}>
{firstButtonText}
</button>
</div>
<div
className={`${style['button']} ${
style[`-color-${secondButtonBackgroundColor}`]
}`}>
<button onClick={onClickSecondButton} type={'button'}>
{secondButtonText}
</button>
</div>
</div>
);
};
export default DoubleButtonModal;
このモーダルは必ずボタンを2つもつので、すべての props が必須になりました。
また、props の値による表示の制御がなくなり、結合度でいうと データ結合 となりました。
例1 と比べてコード量や見た目はそう変わらないように感じるかもしれませんが、結合度という指標で見ると確かに改善されている言えます。
また、そもそもボタンの属性を可変にする必要があまりない場合には、専用のコンポーネントを用意することでさらに結合度を下げることができます。
例えば、ほとんどのモーダルが OKボタン と キャンセルボタン をもつ場合などです。
例3:決まったボタンを持つモーダルのコンポーネント
OKボタン、CANCELボタン専用のモーダルコンポーネントはこのようになります。
type OkCancelModalProps = {
isOpen: boolean;
onClickOkButton: MouseEventHandler<HTMLButtonElement>;
onClickCancelButton: MouseEventHandler<HTMLButtonElement>;
};
const OkCancelModal: React.FC<OkCancelModal> = ({
isOpen,
onClickOkButton,
onClickCancelButton,
}) => {
return (
<div className={`${style['modal-default']} ${!isOpen && style['-hidden']}`}>
<div className={`${style['button']} ${style[`-color-blue`]}`}>
<button onClick={onClickOkButton} type={'button'}>
OK
</button>
</div>
<div className={`${style['button']} ${style[`-color-gray`]}`}>
<button onClick={onClickCancelButton} type={'button'}>
CANCEL
</button>
</div>
</div>
);
};
export default OkCancelModal;
使うときは
const Index: NextPage = () => {
return (
<div className={style['modal-default']}>
<OkCancelModal
isOpen={isOpen}
onClickOkButton={() => onClickOkButton()}
onClickCancelButton={() => onClickCancelButton()}>
</OkCancelModal>
</div>
);
};
export default Index;
このようになります。
props が減ってより疎結合になりました。
見た目もスッキリして扱いやすくなったと思います。
まとめ
結合度という指標を用いて React のコードのよさを評価できました。
良いコードとは何か。を考える基準の1つとして結合度を取り入れられるとわかりました。