🤷♂️ 凝集度とは
みなさん、おはようございます!
さて、いきなりですが、みなさん
プログラミングにおける、凝集度とはご存知でしょうか?
「知っとるわい!!」との声が聞こえてきそうですが、
かく言う私は、言葉は知っていても、細かい部分まで理解することができていませんでした
と言うわけで、凝集度に関してReactのコードを混ぜながら解説しましたのでご覧ください〜!
もし間違いあればコメントいただけると嬉しいです
結合度編もあるよ!
🤔 凝集度って?
- それは一つのモジュールやコンポーネント、関数、クラスなどが、どれだけ単一の責任や機能に集中しているかを測る指標
- 一つのモジュールの責任を減らすという考え方
🤔 凝集度が高い状態とは?
- そのモジュールや関数が一つの明確な目的に専念していて、それ以外のことを行わない状態のこと
🤔 凝集度が低いと何が悪いの?
- 一つのコンポーネントが多くの責任を負っていること
- コンポーネントの再利用性が下がる
- テストが煩雑になる
- 新しい機能追加が難しくなる
- 既存機能の変更も難しくなる
🤔 凝集度が高いと何が良いの?
- 再利用性が高まる
- 単一の責任を持つコンポーネントは、他のコンポーネントやプロジェクトで簡単に再利用できる
- メンテナビリティが上がる
- 何かに変更があった場合、影響を受ける部分が少なくなる
- 故にメンテナンスのコストが下がる
- 可読性が上がる
- 凝集性の高いコードは、単一の責任を負えているコード
- 故に何をしているのかが理解しやすい
- 新しい開発者の参入障壁も下がる
- テスタビリティ
- 単一の責任を負っているため、単体テストが書きやすい
- 結果、ソフトウェア全体の品質も向上する
📏 凝集度を測る指標
- 凝集度を測る指標は7つ存在する
- 7に行くにつれ、凝集度が高い状態と言える
- 偶発的凝集
- 論理的凝集
- 時間的凝集
- 手続き的凝集
- 通信的凝集
- 逐次的凝集
- 機能的凝集
⚖️ 凝集度の使い分け
- 以下のように使い分けることができる
- 偶発的凝集 👈 必ず避けるべき😡
- 論理的凝集 👈 可能な限り避けるべき😑
- 時間的凝集 👈 可能な限り小さく保つ🥺
- 手続き的凝集 👈 可能な限り小さく保つ🥺
- 通信的凝集 👈 可能な限り小さく保つ🥺
- 逐次的凝集 👈 理想的🥳
- 機能的凝集 👈 理想的🥳
偶発的凝集 (必ず避けるべき😡)
🤔 偶発的凝集性ってなに?
- 偶発的凝集性とは、関係性のない幾つかの機能が一つのコンポーネントなどにまとめられている状態🤮
🤔 何がだめ?
- コードの再利用性が下がる
- 可読性が下がる
- テスタビリティが低下する
🧑💻 偶発的凝縮のコード例
- この例ではユーザー情報とポスト情報を一つのコンポーネントで取得し表示している
- つまり、一つのコンポーネントが一つの責任を負えていない状態
function UserProfile() {
useEffect(() => {
// ユーザー情報取得
// ポスト情報取得
}, []);
// ユーザー情報を表示
const renderUserInfo = () => { ... };
// ポスト情報を表示
const renderPosts = () => { ... };
// ユーザー情報とポスト情報を同時に表示
return (
<div>
{renderUserInfo()}
{renderPosts()}
</div>
);
}
- 以下のように独立しているのが理想的
- ユーザー情報の取得、表示
- ポスト情報の取得、表示
🧑💻 偶発的凝縮の改善例
- 以下のようにコンポーネントを分割する
- それにより各コンポーネントが単一の責任を負えるようになる
- これが凝縮度が高まり、コードの再利用性、可読性、テスタビリティが向上する
function UserInfo() {
useEffect(() => {
// ユーザー情報取得
}, []);
return ( ... )
}
function UserPost() {
useEffect(() => {
// ポスト情報取得
}, []);
return ( ... )
}
function UserProfile() {
return (
<div>
<UserInfo />
<UserPost />
</div>
);
}
論理的凝集 (可能な限り避けるべき😑)
🤔 論理的凝集ってなに?
- 論理的凝集とは、関連性は低いが一定のカテゴリや条件でまとめられた、複数の機能がモジュールやコンポーネントに含まれている状態🤢
🤔 何がダメなの?
- コードの再利用性や可読性が低くなる
🧑💻 論理的凝集のコード例
- 以下のコードは、ユーザーに関するアクションでまとめられている
- 一見まとまりがいいようにも思える
- が、それぞれのアクション自体で関連性は低い状態
function UserActions({ userId }) {
const showProfile = () => { /* プロフィールを表示するロジック */ };
const logout = () => { /* ログアウトするロジック */ };
const changeSettings = () => { /* 設定を変更するロジック */ };
return (
<div>
<button onClick={showProfile}>Show Profile</button>
<button onClick={logout}>Logout</button>
<button onClick={changeSettings}>Change Settings</button>
</div>
);
}
🧑💻 改善案
- 同じく責任ごとに独立させるべき
// ShowProfileButtonコンポーネント
function ShowProfileButton() {
const showProfile = () => { /* プロフィールを表示するロジック */ };
return <button onClick={showProfile}>Show Profile</button>;
}
// LogoutButtonコンポーネント
function LogoutButton() {
const logout = () => { /* ログアウトするロジック */ };
return <button onClick={logout}>Logout</button>;
}
// ChangeSettingsButtonコンポーネント
function ChangeSettingsButton() {
const changeSettings = () => { /* 設定を変更するロジック */ };
return <button onClick={changeSettings}>Change Settings</button>;
}
// UserActionsコンポーネント
function UserActions() {
return (
<div>
<ShowProfileButton />
<LogoutButton />
<ChangeSettingsButton />
</div>
);
}
時間的凝集 (可能な限り小さく保つ🥺)
🤔 時間的凝集ってなに?
- 時間的凝集とは、ある特定の時間または状態でのみ関連がある処理が、一つのモジュールやコンポーネントにまとめられている状態
🤔 何がダメなの?
- 再利用性や、可読性、メンテナビリティが低い
🧑💻 時間的凝集のコード例
- アプリケーションの初期化処理が時間的凝集になっている
- ユーザーデータのロード
- 設定のロード
- 未読メッセージのロード
- 時間的に見ると関連しているが、機能的な関連はそれほどない
function AppInitialization() {
useEffect(() => {
loadUserData();
loadAppSettings();
loadUnreadMessages();
}, []);
const loadUserData = () => { /* ユーザーデータをロードするロジック */ };
const loadAppSettings = () => { /* アプリケーション設定をロードするロジック */ };
const loadUnreadMessages = () => { /* 未読メッセージをロードするロジック */ };
return (
<div>
{/* ... */}
</div>
);
}
🧑💻 改善案
- 個々のコンポーネントや関数に分割
- 責任の範囲を明確にさせる
- これにより再利用性や可読性、テスタビリティにおいても改善することができる
// UserDataLoaderコンポーネント
function UserDataLoader() {
useEffect(() => {
// ユーザーデータをロードするロジック
}, []);
return null;
}
// AppSettingsLoaderコンポーネント
function AppSettingsLoader() {
useEffect(() => {
// アプリケーション設定をロードするロジック
}, []);
return null;
}
// UnreadMessagesLoaderコンポーネント
function UnreadMessagesLoader() {
useEffect(() => {
// 未読メッセージをロードするロジック
}, []);
return null;
}
// AppInitializationコンポーネント
function AppInitialization() {
return (
<div>
<UserDataLoader />
<AppSettingsLoader />
<UnreadMessagesLoader />
{/* ... */}
</div>
);
}
手続き的凝集 (可能な限り小さく保つ🥺)
🤔 手続き的凝集ってなに?
- 手続き的凝集とは、特定の手続きやタスクを順番に実行するために、一つのモジュールやコンポーネントにまとめられている状態
🤔 何がダメなの?良いの?
- 凝集性はそれなりに高いとされている
- ただ、コンポーネントが多くの責任を持っている場合では、可読性や再利用性に問題が生じる
🧑💻 手続き的凝集のコード例
- 以下のコードはユーザー登録のための手続きを行なっている
- 手続きとは以下の流れのこと
- バリデーション
- APIリクエスト
- 成功、失敗時のハンドリング
- これらを一つのコンポーネントで行なっている
function UserRegistration() {
const [formData, setFormData] = useState({});
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const validateData = () => {
// バリデーションロジック
};
const submitForm = async () => {
if (validateData()) {
try {
/* 成功時の処理 */
} catch (e) {
/* エラー時の処理 */
}
} else {
/* バリデーションエラー時の処理 */
}
};
return (
<div>
{/* フォームフィールド */}
<button onClick={submitForm}>Register</button>
{error && <div>{error}</div>}
{success && <div>Registration successful!</div>}
</div>
);
}
🧑💻 改善案
- 各手続きをカスタムフックや独立した関数に分割する
- コンポーネントから出し、カスタムフック化
-
validateData
⇒useValidation
-
submitForm
⇒useSubmitForm
-
- コンポーネントから出し、カスタムフック化
// useValidationカスタムフック
const useValidation = (formData) => {
// バリデーションロジック
return true;
};
// useSubmitFormカスタムフック
const useSubmitForm = (formData, validate) => {
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const submitForm = async () => {
if (validate()) {
try {
/* 成功時の処理 */
} catch (e) {
/* エラー時の処理 */
}
} else {
/* バリデーションエラー時の処理 */
}
};
return [submitForm, error, success];
};
// UserRegistrationコンポーネント
function UserRegistration() {
const [formData, setFormData] = useState({});
const isValid = useValidation(formData);
const [submitForm, error, success] = useSubmitForm(formData, isValid);
return (
<div>
{/* フォームフィールド */}
<button onClick={submitForm}>Register</button>
{error && <div>{error}</div>}
{success && <div>Registration successful!</div>}
</div>
);
}
通信的凝集 (可能な限り小さく保つ🥺)
🤔 通信的凝集ってなに?
- 通信的凝集とは、一連の処理や同じデータ、または入力を操作、利用するために1つのコンポーネントや、モジュールにまとめられている状態
🤔 何がダメなの?
- 通信的凝集は中程度の凝集性があるとされている
- しかし、処理の独立性や再利用性が損なわれる可能性がある
🧑💻 通信的凝集のコード例
- 以下は、ユーザーのプロフィールに関する幾つかの異なる処理を行なっている
- プロフィールの表示
- 編集
- 削除
- これらは全て
userData
という共通のデータを使用している
function UserProfile({ userId }) {
const [userData, setUserData] = useState(null);
useEffect(() => {
// userDataをロードするロジック
}, [userId]);
const editProfile = () => { /* userDataを編集するロジック */ };
const deleteProfile = () => { /* userDataを削除するロジック */ };
return (
<div>
{/* userDataを表示するロジック */}
<button onClick={editProfile}>Edit Profile</button>
<button onClick={deleteProfile}>Delete Profile</button>
</div>
);
}
🧑💻 改善案
- 各処理を独立したコンポーネントに分けることで、凝集性を高められる
// ProfileViewerコンポーネント
function ProfileViewer({ userData }) {
// userDataを表示するロジック
return (
<div>{/* userDataを表示 */}</div>
);
}
// ProfileEditorコンポーネント
function ProfileEditor({ userData, setUserData }) {
const editProfile = () => {
// userDataを編集するロジック
};
return <button onClick={editProfile}>Edit Profile</button>;
}
// ProfileDeletorコンポーネント
function ProfileDeletor({ userData, setUserData }) {
const deleteProfile = () => {
// userDataを削除するロジック
};
return <button onClick={deleteProfile}>Delete Profile</button>;
}
// UserProfileコンポーネント
function UserProfile({ userId }) {
const [userData, setUserData] = useState(null);
useEffect(() => {
// userDataをロードするロジック
}, [userId]);
return (
<div>
<ProfileViewer userData={userData} />
<ProfileEditor userData={userData} setUserData={setUserData} />
<ProfileDeletor userData={userData} setUserData={setUserData} />
</div>
);
}
逐次的凝集 (理想的🥳)
🤔 逐次的凝集ってなに?
- 逐次的凝集とは、一連の処理が順番に実行され、一つの処理の出力が、次の処理の入力となるようにまとめられている状態。
🤔 何がダメなの?
- 逐次的凝集は、処理が密接に結びついているため、凝集性は高いとされている
- が、コンポーネントが多くの責任を持つと可読性、再利用性に問題が生じる
🧑💻 逐次的凝集のコード例
- データのフェッチ、変換、保存といった
- 一連の処理を順番に行なっている
function DataProcessing() {
const [data, setData] = useState(null);
const fetchData = async () => {
// データをAPIから取得
const rawData = await fetch("/api/data").then(res => res.json());
return rawData;
};
const transformData = (rawData) => {
// データを変換
return rawData.map(item => item * 2);
};
const saveData = (transformedData) => {
// 変換後のデータを保存
localStorage.setItem("processedData", JSON.stringify(transformedData));
};
useEffect(() => {
// 前の処理の出力が、次の処理の入力になっている
// データを取得
fetchData().then(rawData => {
// 取得したデータを入力し、変換したデータを出力
const transformedData = transformData(rawData);
// 変換したデータを入力し、localStrageを保存
saveData(transformedData);
// 変換したデータを入力し、dataの値を更新
setData(transformedData);
});
}, []);
return (
<div>
{/* データを表示 */}
{data && data.map((item, index) => <div key={index}>{item}</div>)}
</div>
);
}
🧑💻 改善案
- 処理をコンポーネントから出し、カスタムフックとして定義する
-
DataProcessing
コンポーネントでは、カスタムフックを呼ぶだけになる - カスタムフックにまとめることで、コンポーネントの役割が明確になる
- あくまで表示を司るだけ
- これにより、再利用性が高まりテストが容易になる
- 逐次的凝集は既に凝集性が高いと言えるが、このようなリファクタリングで更に可読性やメンテナビリティを上げることができる
// useDataProcessingカスタムフック
const useDataProcessing = () => {
const [data, setData] = useState(null);
const fetchData = async () => {
const rawData = await fetch("/api/data").then(res => res.json());
return rawData;
};
const transformData = (rawData) => {
return rawData.map(item => item * 2);
};
const saveData = (transformedData) => {
localStorage.setItem("processedData", JSON.stringify(transformedData));
};
useEffect(() => {
fetchData().then(rawData => {
const transformedData = transformData(rawData);
saveData(transformedData);
setData(transformedData);
});
}, []);
return data;
};
// DataProcessingコンポーネント
function DataProcessing() {
const data = useDataProcessing();
return (
<div>
{/* データを表示 */}
{data && data.map((item, index) => <div key={index}>{item}</div>)}
</div>
);
}
✋ 逐次的凝集と手続き的凝集って似てない!?何が違うの!?
- 一連の処理が含まれているという点は確かに似ている
- が、主には以下のような違いがある
🫰 逐次的凝集
- 処理の流れが順番に実行される
- 一つの処理の出力が次の処理の入力として必要
- 逐次的凝集では各処理が順序を守って実行される必要がある
- データをAPIからフェッチ
- フェッチしたデータを変換
- 変換したデータを保存
🫰 手続き的凝集
- 処理が一連のステップで構成される
- が、各ステップ間でデータの受け渡しは必須ではない
- 一つの目的のために、複数の独立した処理が組み合わされる
- ユーザーデータをバリデーションする
- バリデーションが通ったらデータをデータベースに保存する
- 保存が成功したら、ログを出力する
- 上記のステップは、ユーザーデータの保存という一つの目的に対して行われる
- が、それぞれの処理は独立している
🫰まとめ
- 逐次的凝集
- 各処理が順番に依存関係を持っている
- 手続き的凝集
- 各処理が目的に対して独立している
- 必ずしも順番や依存関係には縛られていない
🙆♂️ 共通して行える対策
- どちらに対しても、関数の分割やカスタムフックの使用で、責任を分割させることで凝集性を高めることができる
機能的凝集 (理想的🥳)
🤔 機能的凝集ってなに?
- 機能的凝集とは、同じ関数やコンポーネント内のすべての操作が、単一の目的またはタスクに集中している状態
🤔 何がダメなの?
- 何もダメじゃない
- これが最も理想的な設計
🧑💻 ダメなコード例
- これは機能的凝縮ではない状態
- ユーザー情報の表示や処理、取得など複数の責任を持っている
- 今まで↑の項目で説明してきたようなコード
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// ユーザー情報をAPIから取得
fetch(`/api/users/${userId}`).then(res => res.json()).then(data => setUser(data));
}, [userId]);
useEffect(() => {
// ユーザーの年齢に基づいて通知
if (user && user.age < 18) {
alert("You must be 18 or older!");
}
}, [user]);
return (
<div>
<h1>User Profile</h1>
{user && (
<>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
{/* ...その他のユーザー情報 */}
</>
)}
</div>
);
}
🧑💻 改善案
- それぞれが独立した関数やカスタムフックに分かれている
- 一つの目的に集中している状態
// ユーザー情報を取得するカスタムフック
const useFetchUser = (userId) => {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`).then(res => res.json()).then(data => setUser(data));
}, [userId]);
return user;
};
// ユーザーの年齢に基づいて通知を行うカスタムフック
const useAgeNotification = (user) => {
useEffect(() => {
if (user && user.age < 18) {
alert("You must be 18 or older!");
}
}, [user]);
};
function UserProfile({ userId }) {
const user = useFetchUser(userId);
useAgeNotification(user);
return (
<div>
<h1>User Profile</h1>
{user && (
<>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
{/* ...その他のユーザー情報 */}
</>
)}
</div>
);
}
🙌 まとめ
- 凝集度の高い状態とは?
- 関数やコンポーネントが、単一の責任を負っている状態であること
- つまり、機能的凝集に限りなく近い状態であること
- これが最も凝集度が高く理想的な設計である
- これにより以下のメリットが得られる
- 可読性の向上
- 再利用性の向上
- メンテナビリティの向上
- テスタビリティの向上
▼ 参考
良いコードとは何か - エンジニア新卒研修 スライド公開
https://note.com/cyberz_cto/n/n26f535d6c575