🐡

良いコードとは何か 凝集度についてまとめ

に公開

はじめに

先日、サイバーエージェントで一般公開されている新卒研修スライドを見ていたところ 凝集度と結合度について説明されていました。 単語自体は聞いたことがありましたが、内容を知らなかったので学習しました。

文量が多くなるので、今回は凝集度に絞ってまとめようと思います。

凝集度

関数の役割の少なさを表しています。高いほうが良く、低いと悪いとされています。

1つの関数に色んな処理が含まれていると、 再利用しづらかったり、読み手側の負担になることは想像できます。

凝集度には7種類あるので悪い順に説明します。

偶発的凝集

無作為に処理が集められた関数のこと。

var b int
function twice(a int){
// 2倍とは関係ない処理
b = a * 100
return a*2
}

function main(){
echo twice(5)
echo b
}

twiceは引数の数を2倍にして返す関数なのに、bにも値を代入しています。 関数名から想像できないし、bに影響があるため再利用もしづらい。

改善案

function twice(a int){
return a*2
}

function main(){
echo twice(5)
echo b * 100
}

これでtwiceは関数名通りの機能のみになりました。

論理的凝集

フラグで処理内容を切り替える関数。

function sendMessage(flag string,text string,stampId Int){
    if(flag = "text"){
        // テキスト送信処理
    }elseif(flag = "stamp"){
        // スタンプ送信処理
    }
}

フラグによってテキストやスタンプ送信の処理を行う。 テキスト送信する時には、stampIdの引数が必要無いし、逆もそう。

また、flagがどういった挙動するのか関数名から分からないし、 画像送信など、他の種類も出てきた時に複雑化してしまう。

sendTextsendStampに処理を分けることで改善を行う。

時間的凝集

機能に関連はないが、 初期化時など近い時間で実行する処理をまとめた関数。

function init(){
    //DB設定
    //config設定
    //viewの初期化
}

初期化時は同時実行してもよいが、再利用する再に必要ない処理が走ってしまうので使いづらい。

機能毎に関数化して呼ぶ形にすることで再利用を可能にする。

ただし、再利用がほぼ無かったりすることもあるので判断が難しい。

手続き的凝集

時間的凝集に似ているが、実行順番にも意味がある関数。

function openFile(){
bool = existFile(fileName)
if(!bool){
    //エラー処理
    //return
}

fileOpen(fileName)
}

デメリットは時間的凝集と同じく、機能に関係無いものが含まれること。

改善方法は時間的凝集と同様に関数で切り出す。

通信的凝集

機能的な関連は無いが、同じ時間で同じ値に対して処理をする関数。

function changeA(a){
    changeA1(a)
    changeA2(a)
    changeA3(a)
}

時間的凝集に似ている。 時間的凝集と異なるのは、同じ値に対して処理を行う点。 行数が長くなるなら、関数化して呼ぶようにする。 デメリットは再利用がしづらいこと。

逐次的凝集

機能的な関連は無いが関連する値を受け渡して処理する関数。

function changeA(a){
    a1 = changeA1(a)
    a2 = changeA2(a1)
    return changeA3(a2)
}

デメリットは複数の処理が含まれるため、再利用がしづらいこと。

機能的凝集

単一の機能を処理する関数。理想形。

function twice(a int){
return a*2
}

まとめ

凝集 判断
偶発的凝集 絶対ダメ
論理的凝集 極力避けたい。一番よくある
時間的凝集 状況に応じてOK
手続き的凝集 状況に応じてOK
通信的凝集 状況に応じてOK
逐次的凝集 状況に応じてOK
機能的凝集 理想

もう少し深ぼり

時間,手続き,通信,逐次的凝集

4種類は似ているため、まとめて時間的凝縮と呼ぶことが多い。

まず前提として悪ではない。 大切なのは関数の詳細を書かないこと。 詳細は別途機能的凝集の関数に切り出せばOK。

論理的凝集

フラグで分けるのではなく、関数を分けてしまうのが良い。

しかし現実的には以下の理由からしたくなる。

  1. 実装の重複を避けるため。 これは、処理を関数に切り出すことで対応可能。

  2. 機能追加忘れが発生する。

    • 「共通化による変更箇所の局所化」
    • 「論理的凝集の回避による影響範囲の局所化」 のトレードオフになる。

    基本的に、後者を優先したい。 テストコードを実装することで、機能追加忘れは防止できるため。

前者を優先した場合、複雑さが増し負債として積み上がる。

  1. 実行順序をDRYにするため 2に似ている。実行順序を守らせるために共通化しておく。 2と同じ理由なのと、そもそも順序が変わることってあまりない。 テストコードで防止可能。

おわりに

種類と内容については知らなかったが、基本的には守れていたなと思いました。

ただ、プロジェクトが複雑化した時に意識したり、 コードレビューの時に理由付きで説明できるようになったので 整理できて良かったです。

GitHubで編集を提案

Discussion