🤷♂️ 結合度とは
みなさん、おはようございます!
さて、いきなりですが、みなさん
プログラミングにおける、結合度とはご存知でしょうか?
「知っとるわい!!」との声が聞こえてきそうですが、
かく言う私は、言葉は知っていても、細かい部分まで理解することができていませんでした
と言うわけで、結合度に関してReactのコードを混ぜながら解説しましたのでご覧ください〜!
もし間違いあればコメントいただけると嬉しいです
凝集度編もあるよ!
🤔 結合度って?
- 結合度とは、関数、モジュール、コンポーネントなどが、どれだけ他のコンポーネントに依存しているかを示す指標
🤔 結合度が低い状態とは?
- 結合度が低い状態とは、各コンポーネントが互いに独立している状態
- もしくは非常に少ない依存性しか持っていない状態
- 独立性
- 各コンポーネントが、できるだけ自己完結している
- 他のコンポーネントに依存することなく動作する
- 単一責任
- 各コンポーネントはたったひとつの仕事しかしない
- 他のコンポーネントと密接に連携する必要がない
- データの局所性
- データはできる限り、局所的なスコープで保持されている
- グローバルな変数など共有のリソースに依存することが少ない
- 明示的なインターフェイス
- コンポーネント間でやり取りが必要な場合でも、どんなpropsが必要かが明確な状態
- 再利用性
- 低い結合度のコンポーネントは再利用が容易である
- 他のコンポーネントと依存性が少ないため、異なる環境でも使いまわせる
- 容易なテスト
- 独立して動作するコンポーネントは、単体テストが容易である
- 他のコンポーネントに依存しないため、モックやスタブを使用する必要がない
- テストの複雑性が低くなる
🤔 結合度が高いと何が悪いの?
- 結合度が高い状態とは、一つのコンポーネントの変更が、他のコンポーネントにも影響を与えてしまう状態
- メンテナビリティが非常に悪い状態である
🤔 結合度が低いと何がいいの?
- 再利用性が高まる
- コンポーネントが他のコンポーネントに依存していない場合、そのコンポーネントは他の場所でも容易に再利用できる
- 可読性が高まる
- コンポーネントが明確なインターフェイス(Reactでいう
Props
など)を持ち、独立して機能する場合、読みやすく理解しやすいものとなる
- コンポーネントが明確なインターフェイス(Reactでいう
- テスタビリティが高まる
- 独立したコンポーネントは、単体テストがしやすく、依存する外部コンポーネントをモックする必要がなくなる
- メンテナンス性が高まる
- 結合度が低いと、一つのコンポーネントに対する変更が、他のコンポーネントに影響を与えにくくなる
- これにより、修正、追加、変更が容易になる
- 柔軟性が高まる
- 結合度が低いと、システムの一部を変更、拡張する際の柔軟性を高める
- 例として、一部の機能を拡張や改良する場合、影響を受ける範囲が小さいため、安全かつ迅速に変更が行える
- スケーラビリティが高まる
- 独立性が高いコンポーネントは並列開発が容易
- チームが大きくなっても効率的に開発が進められる
- エラーの局所化が行える
- 結合度が低いと、バグやエラーが発生しても、影響が局所的になるため、デバッグが容易となる
📏 結合度を測る指標
- 結合度を測る指標は7つある
- 7に行くにつれ、結合度が低い状態と言える
- 内部結合
- 共通結合
- 外部結合
- 制御結合
- スタンプ結合
- データ結合
- メッセージ結合
⚖️ 結合度の使い分け
- 以下のように使い分けることができる
- 内部結合 👈 必ず避けるべき😡
- 共通結合 👈 可能な限り避けるべき😑
- 外部結合 👈 可能な限り避けるべき😑
- 制御結合 👈 可能な限り避けるべき😑 ❗️一部注意が必要
- スタンプ結合 👈 理想的🥳 ❗️一部注意が必要
- データ結合 👈 理想的🥳
- メッセージ結合 👈 理想的🥳
内部結合 (必ず避けるべき😡)
🤔 内部結合ってなに?
- 内部結合とは、あるコンポーネントが、他のコンポーネントの内部詳細に依存している状態
🤔 何がダメなの?
- 再利用性の低下
- 内部結合がある場合、一つのコンポーネントがもう一つのコンポーネントの内部状態に依存しているため、単独での再利用が困難になる
- 可読性の低下
- コンポーネントが互いの内部状態に依存していると、コードを読んだり理解したりするのが、より困難になる
- メンテナンス性の低下
- 内部結合があると、一方のコンポーネントに変更を加えると、もう一方のコンポーネントにも影響をあたえ、メンテナンスが複雑になる
- テスタビリティの低下
- 内部結合があると、単体テストを書く際に、他のコンポーネントの状態や内部構造を考慮する必要が生じる
- これにより、テスタビリティの低下が起こる
- リファクタリングの制限
- 内部状態に依存していると、その部分が変更されると即座に他のコンポーネントにも影響を与える可能性がある
- これが、将来的なリファクタリングや機能拡張を制限する可能性がある
- ローカルの原則の違反
- 一般的には、あるコンポーネントが必要する情報や依存関係は、そのコンポーネント自身かその親コンポーネントが持っているべき 👉 ローカルの原則
- 内部結合はこれに違反し、コンポーネントの独立性が失われる
🧑💻 内部結合のコード例
- 以下の場合、
ParentComponent
がChildComponent
の内部状態に依存している
function ChildComponent() {
const [internalState, setInternalState] = React.useState("initial value");
// 何らかの内部ロジック
return (
<div>{internalState}</div>
);
}
function ParentComponent() {
// ...
return (
<div>
{/* ParentComponentはChildComponentの内部状態に依存する */}
<ChildComponent someProp={/* ... */} />
<button onClick={() => /* ここでChildComponentの内部状態を変更するロジック */}>
Update Child
</button>
</div>
);
}
🧑💻 改善案
- 結合度を下げるには、内部状態を
ParentComponent
で管理する -
ChildComponent
にはProps
で必要な情報を渡す
function ChildComponent({ value }) {
// 何らかの内部ロジック
return (
<div>{value}</div>
);
}
function ParentComponent() {
const [state, setState] = React.useState("initial value");
return (
<div>
<ChildComponent value={state} />
<button onClick={() => setState("new value")}>
Update Child
</button>
</div>
);
}
共通結合 (可能な限り避けるべき😑)
🤔 共通結合ってなに?
- 共通結合とは、複数のコンポーネントが同一のグローバル状態に依存している状態
- 共通結合は避けるべきだが、設定値やテーマなど、避けられない場合もある
- だから可能な限り避けるべき
🤔 何がダメなの?
- 再利用性の低下
- 共通結合があると、一つのコンポーネントを他のプロジェクトで再利用するのが困難になる
- 共通の状態に依存しているため、そのコンポーネント単体で取り出して再利用すると、依存していた共通状態が欠けることになる
- テストの困難性
- 共通の状態に依存するコンポーネントは、その状態を正確にセットアップする必要がある
- 故にテストが複雑化する可能性がある
- コードの可読性
- コンポーネントが外部の状態に依存することで、そのコンポーネントが何に影響を受けているのか、どのように動作するのかの理解が難しくなる
- 大規模なプロジェクトでは、このような依存関係が多くなると、コードの全体像を把握するのが困難となる
- リファクタリングの制限
- 共通の状態に依存している場合、その状態になんらかの変更を加えると、依存しているコンポーネント全てに影響を与える
- この状態が将来起こりうるリファクタリングや拡張に制限を与える可能性がある
- 並行性の問題
- 共通の状態があると、多くのコンポーネントがその状態を変更可能であるため、予期せぬバグや状態の不整合を引き起こす可能性がある
🧑💻 共通結合のコード例
- 以下の例は、
ComponentA
とComponentB
がグローバル変数のglobalState
に依存している
let globalState = "initial value";
function ComponentA() {
return <div>{globalState}</div>;
}
function ComponentB() {
return (
<button onClick={() => { globalState = "new value"; }}>
Update Global State
</button>
);
}
function App() {
return (
<>
<ComponentA />
<ComponentB />
</>
);
}
🧑💻 改善案
- Reactの
ContextAPI
や状態管理のライブラリを使用することで改善できる - こうすることで、
ComponentA
とComponentB
は共通の変数に依存しているが、その依存関係は明示的かつ制御されたものとなる - これにより再利用性やテスタビリティも向上する
- また、
ContextAPI
などを使用すると、共通の状態へのアクセスが特定の場所(root付近)で管理されるため、コードの可読性やメンテナンス性も向上する
import React, { createContext, useContext, useState } from "react";
const GlobalStateContext = createContext();
function ComponentA() {
const globalState = useContext(GlobalStateContext);
return <div>{globalState}</div>;
}
function ComponentB() {
const globalState = useContext(GlobalStateContext);
const setGlobalState = useContext(GlobalStateContext);
return (
<button onClick={() => setGlobalState("new value")}>
Update Global State
</button>
);
}
function App() {
const [globalState, setGlobalState] = useState("initial value");
return (
<GlobalStateContext.Provider value={[globalState, setGlobalState]}>
<ComponentA />
<ComponentB />
</GlobalStateContext.Provider>
);
}
外部結合 (可能な限り避けるべき😑)
🤔 外部結合ってなに?
- 外部結合とは、一つのモジュールが別のモジュールのAPIやデータ構造などの外部インターフェイスに依存している状態
- Reactでは、コンポーネントが外部から取得したデータやサービスに依存している場合、これに該当する
🤔 何がダメなの?
- これも必ずしもダメなわけではないし、避けられない場面もあるが、リスクは生じる
-
可搬性の低下
- 外部システムに依存する場合、そのシステムが変更されると影響を受ける可能性がある
- テスタビリティの低下
- 外部システムに依存する場合、テストが困難になる可能性がある
- スタブやモックを用意する必要があり複雑性が増す
- メンテナンス性の低下
- 外部のライブラリに依存すると、そのライブラリの更新や変更に対応しきれない場合もある
- 再利用性の低下
- 外部に依存するコードは、その依存性がないと再利用できない
- 可読性と理解性の低下
- 外部に依存すると、その動作やデータ構造を理解する必要があり、コード理解の難易度が増す
🧑💻 外部結合のコード例
- 以下の例では、
UserProfile
コンポーネントが外部API(getUserData
)に依存している
// 外部APIからユーザデータを取得する関数
const getUserData = async (userId) => {
// API呼び出しの仮定
return fetch(`/api/users/${userId}`).then((res) => res.json());
};
function UserProfile({ userId }) {
const [userData, setUserData] = React.useState(null);
React.useEffect(() => {
getUserData(userId).then((data) => {
setUserData(data);
});
}, [userId]);
if (!userData) {
return <div>Loading...</div>;
}
return <div>{`User name is ${userData.name}`}</div>;
}
🧑💻 改善案
- 外部との連携部分をカスタムフックに隠蔽する
- そうすることでコンポーネント自体はより独立した形となる
-
getUserData
をカスタムフックにし外部化している
function useUserData(userId) {
const [userData, setUserData] = React.useState(null);
React.useEffect(() => {
getUserData(userId).then((data) => {
setUserData(data);
});
}, [userId]);
return userData;
}
function UserProfile({ userId }) {
const userData = useUserData(userId);
if (!userData) {
return <div>Loading...</div>;
}
return <div>{`User name is ${userData.name}`}</div>;
}
制御結合 (可能な限り避けるべき😑 ❗️一部注意が必要)
🤔 制御結合ってなに?
- 制御結合とは、あるモジュールが別のモジュールの制御フロー(例えば、条件分岐やループ)に影響を与える形で結合されている状態
🤔 何がダメなの?
- 可読性の低下
- 制御結合が発生している場合、一方のモジュールがもう一方のモジュールの内部ロジックや状態に強く依存している
- そのため、コードを理解する難易度が上がる
- メンテナンス性の低下
- あるモジュールで何らかの変更が行われた場合、それが制御結合をもつ他のモジュールに影響を与える
- これにより、小さな変更であっても多くのモジュールを修正する必要が生じる
- 再利用性の低下
- 制御結合が発生しているモジュールは他のコンテキストで再利用する際にも、その制御ロジックを含めなければならない
- よって、再利用が難しくなる
- テスタビリティの低下
- 制御結合があると、単体テストが書きづらくなる
- なぜなら、一方のモジュールのテストを行う際に、制御結合によって影響を受ける他のモジュールも考慮しなければならないから
- 拡張性の低下
- 制御結合があると、新しい機能追加や、既存機能の変更の際に、それが影響を与えるその他モジュールも考慮し、手を加える必要がある
- よって拡張性の低下につながる
🧑💻 制御結合のコード例
- 以下の
App
コンポーネントはHeader
とFooter
に対して、layoutMode
というフラグを渡している - このフラグによって
Header
とFooter
のレンダリングが条件分岐されているため、制御結合が発生している
function App() {
const [layoutMode, setLayoutMode] = React.useState("simple");
return (
<div>
<Header layoutMode={layoutMode} />
<main>
{/* メインのコンテンツ */}
</main>
<Footer layoutMode={layoutMode} />
</div>
);
}
function Header({ layoutMode }) {
return (
<div>
{layoutMode === "simple" ? "Simple Header" : "Complex Header"}
</div>
);
}
function Footer({ layoutMode }) {
return (
<div>
{layoutMode === "simple" ? "Simple Footer" : "Complex Footer"}
</div>
);
}
🧑💻 改善案
- 以下は制御フラグを渡す代わりに、何を表示させるのかを具体的なpropsで渡す例
- しかし、実務上こんな場面はそうはないな?
function App() {
return (
<div>
<Header content="Simple Header" />
<main>
{/* メインのコンテンツ */}
</main>
<Footer content="Simple Footer" />
</div>
);
}
function Header({ content }) {
return <div>{content}</div>;
}
function Footer({ content }) {
return <div>{content}</div>;
}
- 以下は
layoutMode
の状態をカスタムフックで制御する例 - 制御ロジックを抽出することで複数のコンポーネントで再利用できる
- 再利用性に対してはカバーできている例
function useLayoutMode() {
const [layoutMode, setLayoutMode] = React.useState("simple");
// レイアウトモードを制御するロジック
return layoutMode;
}
function Header() {
const layoutMode = useLayoutMode();
// レンダリングロジック
}
function Footer() {
const layoutMode = useLayoutMode();
// レンダリングロジック
}
スタンプ結合 (理想的🥳 ❗️一部注意が必要)
🤔 スタンプ結合ってなに?
- スタンプ結合とは、一方のモジュールがもう一方のモジュールのデータ構造(オブジェクトなど)に依存している状態
🤔 何がダメなの?
-
変更の波及
- 一方のデータ構造が変更されると、それに依存する他のモジュールも変更する必要が生じる
- これによりメンテナビリティが低下する
-
可読性の低下
- どのフィールドが実際に使用されているのか、何のためにそのデータ構造が必要なのか不明瞭になる場合がある
-
テスタビリティの低下
- スタンプ結合により、ユニットテストが困難になる場合がある。
- 依存しているデータ構造が複雑であればあるほど、テストケースの設計と維持が厄介になる
-
カプセル化の破壊
- データ構造に対する直接的な依存は、カプセル化の原則に違反している場合がある。
- カプセル化が破壊されると、データの整合性が維持されにくくなる。
💊 カプセル化とは
- オブジェクト指向の基本的な原則のひとつ
- データ(状態)とそのデータを操作するメソッド(振る舞い)を一つのカプセル(オブジェクト)にまとめることで、データへの直接的なアクセスを制御する手法。
- Reactでは、データ、メソッド、カプセルは以下のものが値する
- データ
- ローカルステート … useStateなど
- Props … 親要素から受け取るデータ
- メソッド
- イベントハンドラ … ユーザーからの入力を処理する関数
- エフェクト … useEffect
- カスタムメソッド … 自身で定義した関数
- カプセル
- コンポーネント … ステート、メソッドなどが一つにまとめられたコンポーネント
- カスタムフック … 独自のロジックやステートをカプセル化した関数
- データ
- これらの要素を組み合わせることでReactでもカプセル化を実現できる
- それぞれのコンポーネントやカスタムフックが独自の責任や機能を持ち、外部からはその詳細が隠蔽される形となるべき
🧑💻 スタンプ結合のコード例
-
UserProfile
コンポーネントはApp
コンポーネントで定義されているuser
オブジェクトに依存する状態になっている - こういった構造は往々にしてあって、ダメなわけではない
// Userオブジェクト全体を渡しています。
function UserProfile({ user }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<p>{user.age}</p>
{/* 他にもたくさんのフィールドがある */}
</div>
);
}
function App() {
const user = {
name: 'John',
email: 'john@example.com',
age: 30,
// 他にもたくさんのフィールドがある
};
return <UserProfile user={user} />;
}
🧑💻 改善案
- データが
App
コンポーネントにある状態は望ましくない - そのため、カスタムフックに切り出し、必要なデータのみをコンポーネントに渡す
function useUser() {
// ここでuserを取得する
return {
name: 'John',
email: 'john@example.com',
age: 30,
};
}
function UserProfile() {
const { name, email, age } = useUser();
return (
<div>
<h1>{name}</h1>
<p>{email}</p>
<p>{age}</p>
</div>
);
}
データ結合 (理想的🥳)
🤔 データ結合ってなに?
- データ結合とは、一方のモジュールがもう一方のモジュールにデータを渡すだけの形で結合している状態
🤔 何がダメなの?良いの?
- 一般的には結合度が低く、良い形の設計と言われる
- ただ、注意が必要な場面もある
- 具体的には、Propsで渡すデータの選抜や、Props名などだ
🧑💻 データ結合のコード例
- 以下の例では、
ParentComponent
からChildComponent
にdata
オブジェクトを渡している - これはデータ結合に該当する
// ParentComponent.js
import React from 'react';
import ChildComponent from './ChildComponent';
function ParentComponent() {
const data = {
message: 'Hello',
id: 1,
};
return <ChildComponent data={data} />;
}
// ChildComponent.js
import React from 'react';
function ChildComponent({ data }) {
return (
<div>
<p>{data.message}</p>
</div>
);
}
🧑💻 改善案
- データが多くのフィールドを持っていて、子コンポーネントがその全てを必要としない場合、必要最小限のデータのみを渡すようにする
- また、Props名も
data
などと抽象的な名前にするのではなく、何のデータなのかをわかりやすくするために、Propsの名前を明示的にする
// ParentComponent.js
// ...
return <ChildComponent greetingMessage={data.message} />;
// ChildComponent.js
// ...
function ChildComponent({ greetingMessage }) {
return (
<div>
<p>{message}</p>
</div>
);
}
メッセージ結合 (理想的🥳)
🤔 メッセージ結合ってなに?
- メッセージ結合とは、一方のモジュールがもう一方のモジュールやメソッドや関数を呼び出す形式で結合されている状態
🤔 何がダメなの?良いの?
- この結合は健全ではあるものの、注意は必要である
🧑💻 メッセージ結合のコード例
- 以下の例では、
ParentComponent
がChildComponent
にincrement
関数を渡している -
ChildComponent
はこのメソッドを呼び出すことで親コンポーネントのステートを変更している
// ParentComponent.js
import React, { useState } from 'react';
import ChildComponent from './ChildComponent';
function ParentComponent() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return <ChildComponent onButtonClick={increment} />;
}
// ChildComponent.js
import React from 'react';
function ChildComponent({ onButtonClick }) {
return <button onClick={onButtonClick}>Increment</button>;
}
🧑💻 改善案
- 親コンポーネントで定義されていた
increment
関数をカスタムフックとして抽出する - また、
onButtonClick
など抽象的な関数名で渡さず、onIncrement
などの関数名にし、何をするか明示的にするべきである
// useCounter.js
import { useState } from 'react';
export function useCounter(initialCount = 0) {
const [count, setCount] = useState(initialCount);
const increment = () => setCount(count + 1);
return { count, increment };
}
// ParentComponent.js
import React from 'react';
import { useCounter } from './useCounter';
import ChildComponent from './ChildComponent';
function ParentComponent() {
const { increment } = useCounter();
return <ChildComponent onIncrement={increment} />;
}
🙌 まとめ
- 一貫して言えることは依存を減らし、各コンポーネントやモジュールが互いに独立している状態が理想であるということ
- なぜなら、それにより、再利用性や可読性、メンテナビリティの向上など、多くのメリットを得られるからである
- 健全なプロジェクトであり続けるため、これらを頭に叩き込み、常に結合度の低い状態を築いていくべきである
▼ 参考
良いコードとは何か - エンジニア新卒研修 スライド公開
https://note.com/cyberz_cto/n/n26f535d6c575