iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

凝集度と結合度について調べて見た(part1凝集度編)

こんにちは。iimonでエンジニアをしているhayashiと申します。

普段は主に拡張機能を開発しております。

いいコードを書きたくて最近調べているんですけど、その指標となる凝集度と結合度に

ついて調べたので、今回は凝集度について解説していければと思います。

凝集度とは

  • パッケージ
  • クラス
  • メソッド

上記モジュール内の協調度を示す指標です。

ここで言うメソッドというのは各メソッドを纏めて呼び出しているメソッドだったりを言っていて、定義の仕方によっては広い意味ではモジュールと捉えて使っています。

メソッドはモジュールの中に含まれる小さな単位で、厳密にはモジュールでは無いという考えもありますが、要は定義の仕方なのかなって思います。

凝集度の色々な解説を見るにあたって、メソッドに対してもモジュールという表現をしているので、ここでもその様に表現させて頂きます。

凝集度

凝集度は主に以下の7つの種類に分類されます。

下に行けば行くほど凝集度が高く品質のいいコードとされております。

あくまで上記は一つの指標であり、全てではないということみたいです。

  • 凝集度が高いモジュールは
    • 堅牢性
    • 信頼性
    • 再利用性
    • 可読性

が高いとされており、凝集度が高いコードを書くことが良いコードを書く第一歩と言えるでしょう。

それでは各凝集度について詳しく解説したいと思います。

1. 偶発的凝集

偶発的凝集とは関連のない処理がただ1つのモジュール内にたまたま集まっている状態のことを言います。各処理に関連性がなく、1つにまとめる意味がない。各メソッドを変更する場合も理由がバラバラになる為、一番良くないとされています。

ただ何となくそこに集めて、何となく発火してる感じですね。。。

[例]

以下はuserにアプローチしているものの、特に同じメソッドに纏めるメリットは無いと考えます。

export const hogeHoge = () => {
    loadUserConfig(); // ユーザー設定の読み込み
  userLogAccess(); // ユーザーのログにアクセス
  sendEmail(); // ユーザーへメールを送信
  updateUserBillingInfo(); // ユーザーへの請求情報の更新
}

2. 論理的凝集

論理的に似た様なことするものを集めたモジュールになります。同じカテゴリに属するが、実行される処理がフラグなどで変わる凝集パターンです。責務が分担されていないので保守性が低く、好ましくないとされています。

[例1]

以下はfilenameとそれに対するactionさえ渡せばfileにアプローチしてくれるメソッドの例です。

fileにアプローチをしてくれるって言う意味では同じカテゴリに属してますが、実行される処理自体は大分違ったりするので、保守性が低く、条件が増えていけば増えて行くほど堅牢性、信頼性、再利用性、可読性が下がります。

export const handleFile = (action: 'open' | 'close' | 'delete', filename: string) => {
  if (action === 'open') {
    openFile(filename);
  } else if (action === 'close') {
    closeFile(filename);
  } else if (action === 'delete') {
    deleteFile(filename);
  }
}

[例2]

以下はinputタグにアプローチしていく論理的凝集メソッドの例です。

それぞれの分岐の中でメソッドも作ってない様な例となります。

条件が増えていくと、どんどん保守性が下がりそうなのは見て取れますね。

export const handlePropertyInput = (
    mode: 'insert' | 'upload' | 'delete',
    input: Element | null,
    value: string = '',
) => {
    if (!(input instanceof HTMLInputElement)) throw new Error('input error');
    switch (mode) {
        case 'insert':
            // ...input[type="text"]のvalueに値をセットする処理
            break;
        case 'upload':
            // ... input[type="file"]に画像をセットする処理(base64)
            break;
        case 'delete':
            // ...input[type="text"]のvalueに値を削除する処理
            break;
    }
};

[例3]

以下は渡されたelemによって処理を分岐している論理的凝集メソッドの例です。

例えば、ある現状の実装ではこれだけのアプローチで良いとしても、条件がどんどん増えていく可能性も考えられます。input一つとってもtypeによってアプローチが変わるので、バグの温床になりそうなのが見て取れると思います。

const hoge = (elems: NodeListOf<Element>) => {
    elems.forEach((elem) => {
        const elm = elem
        if (elm instanceof HTMLInputElement) {
            elm.click()
        }
        if (elm instanceof HTMLSelectElement) {
            elm.value = '2'
        }
    })
}

const inputAndSelectElems = document.querySelectorAll('.hoge')
hoge(inputAndSelectElems)

同じカテゴリに属していても、アプローチが違うと増えれば増えるほど細かい条件が増えてきます。アプローチや条件分岐の幅が広ければ広いほど可読性や再利用性が下がるのかなって思います。

3. 時間的凝集

時間的に同じタイミングで発火するものを集めたモジュールです。

[例]

以下は新規登録画面での登録ボタンを押した際の処理の時間的凝集の例です。

  • データベースへのユーザー登録
  • セッション管理
  • 通知処理
  • 分析・トラッキング
  • UI操作

上記の異なる責務が1つの関数に詰め込まれています。

ユーザー登録を行った際に実行したい処理ということは理解できますが、それぞれ行っている事は必ずしも連動していない場合の凝集パターンです。

実行順序を入れ替えても変わらず動作するメソッドが一つでもあったら、それは時間的凝集と判断できそうです。

// ユーザー登録画面において「登録ボタン押下時」に実行される処理がすべて1つの関数に集められている例です:

export const onRegisterSubmit = async (userData: UserData) => {
    try {
        const res = await saveUserToDatabase(userData); // DB保存:ユーザー情報を登録し、その情報を返す
        if (res.status !== 200) throw new Error(`error: ${res.status}`);
        setSession(res) // sessionに情報を保存
        
        await sendWelcomeEmail(res.user.email); // 通知:ウェルカムメール送信
        await sendLineNotification(res.user.slack); // slack通知
        await updateUserCountAnalytics(); // 登録数更新(QuickSite)
        await logUserRegistrationEvent((res.user.id)) // トラッキングイベント(QuickSite)
        openSuccessModal(); // モーダル表示 登録完了
        movesToUserScreen(res.user.id); // ユーザー画面に遷移
    } catch (error) {
        openRejectModal(); // 失敗モーダル表示
    }
};

4. 手順的凝集

順番に実行する必要があるものを集めたモジュールです。

共通のデータは必要としません。順番こそが大事って感じです。

[例]

以下はhogeBtnを追加する権限を見て、権限があればbtnを追加するという手順的凝集の例です。

const addHogeBtn = async () => {
    const isHogePermission = await isHogePermission() // hogeBtnの権限があるかキャッシュやバックエンドに確認
    if (!isHogePermission) return
    insertHogeBtn() // hogeBtnを追加
}

5. 通信的凝集

同じデータを扱うメソッドを集めたモジュールです。

同じデータを共有しているが、各処理が独立している場合に該当するようで順番は変わっても問題ない凝集パターンです。

[例]

以下はuserのアクティビティに関わるものを纏めた処理です。順番が入れ替わったとしても大差ないのが特徴です。

const processUserData = (user: User) => {
  logUserActivity(user); // ユーザーのアクティビティをログに記録
  sendUserNotification(user); // ユーザーに通知を送信
  updateUserStatistics(user); // ユーザーの統計情報を更新
};

それぞれの処理が独立していて、順番が前後しても問題ないのであれば、時間的凝集との違いは同じデータを共有しているか否かなのかと一瞬思ったんですけど、それは本質ではないようです。

同じデータを共有しているということは必然的にそれに関する処理が纏まるという事になるかと思います。なので、独立しているものの関連度が低いものをモジュール化してしまうと凝集度はたちまち下がってしまうので注意が必要かなって思います。

例えば以下は一見、通信的凝集に見える偶然的凝集です。

const processUserData = (user: User) => {
    logUserActivity(user); // ユーザーのアクティビティをログに記録
    sendMarketingEmail(user); // マーケティングメールを送信
    updateUserPreferences(user); // ユーザーの設定を更新
};
  • logUserActivity は、ユーザーの行動を記録するための処理(監視や分析のため)。
  • sendMarketingEmail は、マーケティング目的でメールを送信する処理。
  • updateUserPreferences は、ユーザーの設定を更新する処理。

これらの処理は、単に同じデータuserを引数として受け取っているだけで、処理同士に直接的な関連性がありません。凝集度が最も低い「偶然的凝集」とみなされます。

これらの処理は「たまたま同じ関数にまとめられているだけ」と判断できます。

こうゆうコードを錯覚して書かない為に、あるいは後から触った他のエンジニアが追加実装する時に凝集度を下げない為には、渡すデータを最初からなるべく絞って渡すのが大事かなって思います。

例えば適当なんですけど、そのプロダクト内でprocessと定義しているdataを予め絞って渡す様にしていれば、実装者もRvの時にも気付きやすいかなって思います。

type userProcessDataType =  {
    hoge: string,
    hoge2: string,
    hoge3: string,
}

const processUserData = (userProcessData: userProcessDataType) => {
      logUserActivity(user); // ユーザーのアクティビティをログに記録
   sendUserNotification(user); // ユーザーに通知を送信
      updateUserStatistics(user); // ユーザーの統計情報を更新
};

6. 逐次的凝集

一つの処理の出力が次の処理の入力になる凝集度パターンです。

[例]

以下はformから取ってきたidをセットする例です。

逐次的というくらいなので順番が関係してて、その処理を行ったことにより次の処理が行えます。

const getResister = async () => {
    const form = await this.saveMng.getFormHtml(mngComApiUrl);
    if (!form) throw new Error('form error')
    const Id = await getId(form);
    setId(id);
}

7. 機能的凝集

すべての処理が1つの明確な目的のために密接に連携している理想的な形です。

[例]

以下は合計の請求金額を計算してます。

const calculateInvoice(items: Item[]) => {
  const subtotal = calculateSubtotal(items); // 小計を計算
  const tax = calculateTax(subtotal); // 消費税を計算
  const total = subtotal + tax; // 合計を計算
  return total;
}

[例2]

以下はuser情報を返します。userInfoはuserの情報を返すという機能的凝集で、それ以外のメソッドも名前を返すとかemailを返すとか住所を返すとか、単一的な機能的凝集に纏まってますね。

export const userInfo = () => {
    const name = getUserName()
    const email = getUserEmail()
    const address = getUserAddress()
    return {name, email, address}
}

const getUserName = () => {
    ...
    ...
    return name
}
const getUserEmail = () => {
    ...
    ...
    return email
}
const getUserAddress = () => {
    ...
    ...
    return address
}

上記が主な凝集度のパターンの解説です。

最初の図で示した通り、

  • 偶発的凝集→必ず避けるべき(最悪)

  • 論理的凝集→可能な限り避けるべき(あまり良くない)

  • 時間的凝集→可能な限り小さく保つ(あまり良くない)

  • 手順的凝集→可能な限り小さく保つ(中程度)

  • 通信的凝集→可能な限り小さく保つ(良い)

  • 逐次的凝集→可能な限り小さく保つ(良い)

  • 機能的凝集→理想的(最高)

下に行けば行くほど凝集度が高くなり、上に行けば行くほど凝集度は低くなります。

凝集度が低いコードを見つけたら、可能な限り下に下げていくイメージで凝集度を高められないか意識する必要があると思います。

実装によっては全てを機能的凝集に出来るわけでは無いので、可能な限り凝集度を上げたモジュールに切り出していくイメージで凝集度を上げきれないものは、なるべく小さく保つようにすれば凝集度が上がり、堅牢性、信頼性、再利用性、可読性が向上します。

改善例

ここでは上記で紹介した凝集パターンを改善していくには?っていう例を示したいと思います。あくまで一例なのでご留意下さい。。。

論理的凝集→機能的凝集(その1)

それでは 例で挙げた論理的凝集の凝集度を上げるとします。

以下のように論理的には纏めてますが、分岐が増えれば増えるほど、堅牢性、信頼性、再利用性、可読性、全てが下がりやすいコードです。

[Module.ts]

export const handlePropertyInput = (
    mode: 'insert' | 'upload' | 'delete',
    input: Element | null,
    value: string = '',
) => {
    if (!(input instanceof HTMLInputElement)) throw new Error('input error');
    switch (mode) {
        case 'insert':
            // ...input[type="text"]のvalueに値をセットする処理
            break;
        case 'upload':
            // ... input[type="file"]に画像をセットする処理(base64)
            break;
        case 'delete':
            // ...input[type="text"]のvalueに値を削除する処理
            break;
    }
};

仮に賃料をinputタグのvalueに入れる処理の場合は以下みたいな感じで呼び出されているとします。

[Func.ts]

const setRentVal = () => {
    const rentFee = getRentFee()
    const input = document.querySelector('.rent') as HTMLInputElement;
    handlePropertyInput('insert', input, rentFee);
};

handlePropertyInputメソッドはinputタグにアプローチするって言う面では纏まっていますが、やはり渡された値やelemによってvalueに値を反映したり、imageをuploadしたりなど、振る舞いが大分変わるので、責務が多すぎます。

この場合は単純に全ての処理を切り出してあげて必要に応じて呼び出すようにしてあげれば機能的凝集に凝集度を上げる事が可能です。

無理やり論理的凝集で共通化する必要はないと思います。

[Module.ts]

const inputValue = (input: HTMLInputElement, value: string) => {
    //  inputにvalueを入力
};
const uploadImage = (input: HTMLInputElement, value: string) => {
    // inputに画像をupload(type="file")
};
const removeInputValue = (input: HTMLInputElement) => {
     // inputのvalueを削除
};

賃料を入力場合、画像をuploadする場合、inputValueを全て削除する場合など、呼び出す際にそれぞれ、必要に応じたメソッドの呼び出し方をすれば、機能的凝集と判断できるかなって思います。

[Func.ts]

const setRent = () => {
    const rentFee = getRentFee() // 賃料を取得
    const input = document.querySelector('.rent') as HTMLInputElement;
    inputValue(input, rentFee);
};

const setImage = () => {
    const base64 = getImageBase64() // base64画像を取得
    const imageElem = document.querySelector('.image') as HTMLInputElement
    uploadImage(imageElem, base64);
};

const resetAllInputVal = () => {
    const inputTypeTextElems = document.querySelectorAll('input[type="text"]')
    inputTypeTextElems.forEach((input) => {
        removeInputValue(input as HTMLInputElement)
    })
}

論理的凝集→機能的凝集(その2)

以下はそもそもinputAndSelectElemsとあるようにandとついている時点でエレメントを取得している段階から2つのタグを取っているのが分かるので責務が増えてます。

hogeメソッドに渡したら良い感じにelemに値をセットしてくれるって意味合いで共通化などをしがちですが、メソッドの役割は単一責任であるべきです。

[Module.ts]

const hoge = (elems: NodeListOf<Element>) => {
    elems.forEach((elem) => {
        const elm = elem
        if (elm instanceof HTMLInputElement) {
            elm.click()
        }
        if (elm instanceof HTMLSelectElement) {
            elm.value = '2'
        }
    })
}

[Func.ts]

const inputAndSelectElems = document.querySelectorAll('.hoge')
hoge(inputAndSelectElems)

改善

以下の様にinputにアプローチするものとselectにアプローチするものをメソッドとして分けます。

これにより機能的凝集に凝集度を高めることが可能です。

elemetの取得もそれぞれに絞ってあげれば良いというわけです。

[Module.ts]

const handleInputElements = (elems: NodeListOf<HTMLInputElement>) => {
    elems.forEach((elem) => {
        elem.click(); // input要素に対する処理
    });
};

const handleSelectElements = (elems: NodeListOf<HTMLSelectElement>) => {
    elems.forEach((elem) => {
        elem.value = '2'; // select要素に対する処理
    });
};

[Func.ts]

const inputElems = document.querySelectorAll('input.hoge');
const selectElems = document.querySelectorAll('select.hoge');

handleInputElements(inputElems);
handleSelectElements(selectElems);

時間的凝集→逐次的凝集と機能的凝集

以下の新規登録画面での登録ボタンを押した際の処理の例だとします。

データベースへのユーザー登録、セッション管理、通知処理、分析・トラッキング、UI操作といった異なる責務が1つの関数に詰め込まれています。これにより、関数が「ユーザー登録」という広範な目的を持ちすぎており、ボタンを押した時の処理がまとまっているので時間的凝集と判断出来そうです。

// ユーザー登録画面において「登録ボタン押下時」に実行される処理がすべて1つの関数に集められている例です:

export const onRegisterSubmit = async (userData: UserData) => {
    try {
        const res = await saveUserToDatabase(userData); // DB保存:ユーザー情報を登録し、その情報を返す
        if (res.status !== 200) throw new Error(`error: ${res.status}`);
        setSession(res) // sessionに情報を保存
        
        await sendWelcomeEmail(res.user.email); // 通知:ウェルカムメール送信
        await sendLineNotification(res.user.slack); // slack通知
        await updateUserCountAnalytics(); // 登録数更新(QuickSite)
        await logUserRegistrationEvent((res.user.id)) // トラッキングイベント(QuickSite)
        openSuccessModal(); // モーダル表示 登録完了
        movesToUserScreen(res.user.id); // ユーザー画面に遷移
    } catch (error) {
        openRejectModal(); // 失敗モーダル表示
    }
};

改善後

onRegisterSubmit→逐次的凝集、その他メソッド→機能的凝集

  • それぞれのメソッドを役割毎に切り出し機能的凝集へと凝集度を高めます。
  • そしてresのデータを使って順番に処理を行います。
export const onRegisterSubmit = async (userData: UserData) => {
    try {
      const res = await registerUser(userData); // 登録(DB保存 + セッション)
      await notifyUserOfRegistration(res.user.email, res.user.slack); // 通知(メール + Slack)
      await trackRegistration(res.user.id);  // トラッキング(登録数 + イベント)
      showSuccessUI(res.user.id); // UI処理(モーダル + 遷移)
    } catch (error) {
      openRejectModal();  // エラーモーダル
    }
  };

   // ユーザー登録処理(DB保存 + セッション保存)
  const registerUser = async (userData: UserData) => {
        const res = await saveUserToDatabase(userData); // ユーザーをDBに保存
        if (res.status !== 200) throw new Error(`error: ${res.status}`);
        setSession(res); // セッション保存
        return res;
  };
  
  // 通知処理(メール送信 + Slack通知)
  const notifyUserOfRegistration = async (email: string; slack: string ) => {
    await sendWelcomeEmail(email); // ウェルカムメール送信
    await sendSlackNotification(slack); // Slack通知
  };
  
    // 分析・トラッキング処理
  const trackRegistration = async (userId: string) => {
      await updateUserCountAnalytics(), // 登録数更新
      await logUserRegistrationEvent(userId), // トラッキングイベント送信
  };
  
    // UI:成功時の表示と遷移
  const showSuccessUI = (userId: string) => {
    openSuccessModal(); // 登録成功モーダル
    movesToUserScreen(userId); // ユーザー画面へ遷移
  };

凝集度の判定はそのモジュール内にある一番低い凝集度で判定されるので、上記モジュールは逐次的凝集という種類になるのですが、時間的凝集から、それぞれの役割に応じてメソッドを切り出し、機能的凝集度にしたことにより、時間的凝集だったメソッドも逐次的凝集へと凝集度を高めることができました。

まとめ

記事を書いてみて、なるべく例としていいコード例を出したいなって思って考えてみたものの、実際に自分が苦しんだ実務でのコードを出せるわけでもなく、少し苦戦した部分はあります。

なるべくは伝わるように例を出して見たつもりではありますが、伝わってなければすいません。。。伝わってくれれば幸いです!

僕もそうですが、誰しもが良かれと思って設計を考えるとは思うのですが、結果的には至ってないなって思うことは後から振り返って多々あると思いますし、それに気づけ事が成長だなって個人的には思います。

ただ、プロダクトとしては負債として残り続けるので、それを解消出来るように、そしてなるべく生み出さないように日々精進を続けたいなって思いました。

次回は結合度について記事を書こうと思います。

この記事を読んで興味を持って下さった方がいらっしゃればカジュアルにお話させていただきたく、是非ご応募をお願いします!

iimon採用サイト / Wantedly / Green

参考記事

https://note.com/cyberz_cto/n/n26f535d6c575

https://www.youtube.com/watch?v=-yPPfe13bb0

https://zenn.dev/miya_tech/articles/0dde1228045af6

・Robert C.Martin (2018), Clean Architecture 達人に学ぶソフトウェアの構造と設計, KADOKAWA

https://speakerdeck.com/sonatard/coheision-coupling

 https://t-wada.hatenablog.jp/entry/ward-explains-debt-metaphor

・The Clean Architecture, https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

・世界一わかりやすいClean Architecture, https://www.slideshare.net/AtsushiNakamura4/clean-architecture-release

・Code readability, https://speakerdeck.com/munetoshi/code-readability