凝集度 & 結合度

凝集度(cohesion)

凝集度(cohesion)とはクラス、メソッド内など単一モジュール内の要素間の関連性についての尺度のことを指す

以下のレベルで定義でき、レベルが高いほど堅牢性、信頼性、再利用性、可読性の点で良いとされる

単一責任原則(他含め、SOLID原則自体)を理解する上で、凝集度は知っておくとさらに理解が深まり、今のコードがどの程度のものなのかの物差しにもできる

7つのレベル

-- 低い凝集度 --

レベル1:偶発的凝集(暗号的凝集) レベル2:論理的凝集

-- 中程度の凝集度 --

レベル3:時間的凝集(一時的凝集) レベル4:手順的凝集(手続き的凝集)

-- 高い凝集度 --

レベル5:通信的凝集 レベル6:逐次的凝集 レベル7:機能的凝集

いわゆる「凝集度が高い = 強度が高い」「凝集度が低い = 強度が低い」となる 凝集性が高いと

  • モジュールの独立性が高く
  • 修正しやすく
  • コードを理解しやすい

また凝集度の意識があることで、単一責任原則が当てはめられない場合にもその中でベストな手法を見つけやすくなったりもする

レベル1: 偶発的凝集(暗号的凝集)

関連性のない実装が並べられただけの状態 なぜそこにそのメソッドがあるのか理由が見つからない

cosnt userUtil = () => { // 何をやっているのかわからない const user = getData(); changeOrder(user.name); // オーダーに関する処理 increment(); // 何をインクリメントするのか? }

レベル2: 論理的凝集

特定のフラグを用いて if や switch などで呼び出す処理を場合分けしている状態 内包される命令群の関連性は弱く、モジュール強度は弱くなる

パラメーターの取り扱いでミスを誘いやすい

const registerUser = (user) => { if(user.getCountry() === 'japan') { // 国内ユーザーの登録処理 return; } // 海外ユーザーの登録処理 }

論理的凝集

しかし、局所的に命令を変更させる使い方などでメリットを発揮することもあるため、そういった使い方の時には有効に働く

論理的凝集

レベル3:時間的凝集(一時的凝集)

初期化処理やエラー処理など、あるタイミングで実行する関数の中に幾つかの機能が含まれている状態 機能間にあまり強い関連性はなく、実行の順序を変更しても動作する 再利用性を求めない

const initialize = () => { initConfig(); // 設定の初期化 initData(); // データの初期化 initLogger(); // ログの初期化 }

レベル4:手順的凝集(手続き的凝集)

扱うデータが異なり、実行順序に関連性がある 処理の流れをそのまま詰め込んだようなモジュール 機能が増えるごとに関数が大きくなる

const updateFile = (file) => { checkUserPlan(); // ユーザーに関するチェック checkPermission(); // 権限チェック writeFile(file); // ファイル書き込み }

手順的凝集

レベル5:通信的凝集

特定のデータの処理をする機能の集まりで処理の実行順序は関係がない

const changeImageProfile = (data) => { changeImageName(data); changeImageMeta(data); changeImageRatio(data); }

通信的凝集

レベル6:逐次的凝集

特的のデータを扱う処理の集まりで順番に関係がある ある部分の出力が別の部分の入力となるような部分を集めたモジュール

const minimizeFile() { const file = getFile(); // 出力 const minimizedFile = minimize(file) writeFile(minimizedFile) // 入力 }

逐次的凝集

レベル7:機能的凝集

単一の目的しか持たない 保守性、テスト容易性から見ると最良の状態 できるだけ機能的凝集を目指して作るが、全ての関数を機能的凝集にすることはできないし、必ずしも適切とは言えない

const calcRectangleArea = (width, height) => { return width * height; }

機能的凝集

プラクティス

モジュール独立性を高めるために高めるために、できるだけ 7.機能的強度を目指し、内容によっては 6. 逐次的凝集5. 通信的凝集 もケースに応じて使用わけしつつ、独立性を高めたモジュールを作成していく もし、凝集度が低いモジュールがあった際には、内部の処理を凝集度の高いモジュールに切り出す

もちろん、initialize時の時間的凝集など必然的に発生してしまうものもある。 凝集度の低いモジュールを使わないではなく、なるべく減らしていく考えで進めた方が良さそうに感じる

結合度(Coupling)

結合度(Coupling)とはモジュール同士の関係の密接さを表す指標 結合度(依存度)が高いと依存先の修正の影響を受けやすくなってしまう その為、結合度は低い方が良いとされる

こちらは6段階でのレベル分けがされ、レベルが高いほど疎結合であり、良いモジュールとされます

疎結合の判定にはモジュール間でデータがどのように受け渡しされているかに着目して行う

6つのレベル

レベル1:内容結合 レベル2:共通結合 レベル3:外部結合 レベル4:制御結合 レベル5:スタンプ結合 レベル6:データ結合

レベル1: 内容結合

あるモジュールと他のモジュールが一部(データの変化や命令)を共有するようなモジュール結合の仕方を指す 他モジュールの外部宣言されていないデータを直接参照したり、命令の一部を共有したりするような状況

public Class Printer { public static void print() { System.out.println(Counter.number) // Couter内部の値を直接参照している } } public class Counter { public static int number = 0; public static void increment() { number ++; } }

レベル2: 共通結合

グローバルに定義したデータ(グローバル変数)を幾つかのモジュールが共同使用するような結合の仕方を指す 影響範囲がわかりづらく、あるモジュールで変更を加えたら、同じデータを参照している他のモジュールでバグが発生したりする そのため、コードの読解を非常に難しくし、共通行のデータを通じていろいろなモジュールと繋がってしまうため、再利用性も落ちる

let data = 'banana'; const updateA() { data = 'grape' } cosnt updateB() { data = 'ramen' }

レベル3:外部結合

外部宣言(public宣言された変数も含む)したデータを共有したモジュール間の結合の仕方を指す 例えばライブラリやIOデバイスへの接続手段を複数のモジュールで共有している状態

cosnt getFilterPost = (date) => { const currentDate = day.js().format('YYYY-MM-DD'); // 外部ライブラリに依存してしまっている。他の日付操作ライブラリに変えたいとなったら? const date = dayjs(date).format('YYYY-MM-DD'); posts.filter(post => dayjs(post.date).format('YYYY-MM-DD') === currentDate) }

レベル4:制御結合

パラメーターの1つとしてスイッチ変数を渡し、呼び出されるモジュールがその時に行う機能を指示します。 呼び出し側は呼び出されるモジュールの中身(AとBの場合それぞれの処理の中身)を知っている必要があり、結合度が高くなります 先ほど凝集度の話にもあった レベル2.論理的凝集 になってしまう

const registerUser = (user) => { if(user.getCountry() === 'japan') { // 国内ユーザーの登録処理 return; } // 海外ユーザーの登録処理 }

レベル5:スタンプ結合

モジュールに引数としてクラスやオブジェクトを渡しており、その一部しか使われないケースを指す 不必要なデータも含んだデータ構造ごと受け渡ししているため、呼び出し側から何が使用されているのか理解できず、データ構造が変わっただけでエラーとなってしまい結合度が強くなってしまいます

cosnt findUserEmail(user) { return EmailService.findByUserId(user.id); }

レベル6:データ結合

モジュールで使うデータだけを、数字や文字列などの、プリミティブな値の引数でやりとりする 使う側がそのモジュールが内部でどういった処理をしているのかを知る必要は少なくなる テスト容易性も高い

const getUserItems = (userId, itemType) = { query = new ResorceQuery(); query.addUserFilter(userId); query.addItemTypeFilter(itemType); return itemRepository.findByQuery(query); }

プラクティス

データの受け渡しはできるだけプリミティブな引数で行う できるだけグローバルな変数にデータを置かない。その場でしか必要としないデータはローカル変数に置く

まとめ

  • モジュール内部の話:凝集度
  • モジュール同士の関係性の話:結合度
  • これらの指標を知ることで、今自分が書いているコードがどの程度良いコードなのかを客観的に知ることができる
  • また、リファクタリングの余地を図る一つの指標にもなる
  • とはいえ、早すぎるタイミングで抽象化しすぎるのも個人的には良くないと思っている(変数名考えるコストも高まるし。個人的にはKISSの法則が好き。)ので、どういう程度に付き合っていくべきかはとても難しく経験が必要だなと感じる