プログラムにおける「凝集度」「結合度」ってなに?
恥ずかしながら、"凝集度" と "結合度" という概念を最近知った。
自身が体得している経験と近しいものがあるのかを、噛み砕きながら理解する。
参考文献
凝集度(ぎょうしゅうど)とは?
凝集度は、あるコードがどれだけそのクラスの責任分担に集中しているかを示す尺度である。
とのこと。責任分担への集中度か、なるほど。
凝集度の高いシステムでは、コードの読みやすさと再利用の容易さが増し、複雑さが管理可能な程度に抑えられる。
ふむふむ。SOLID の "単一責任の法則" に近しいものを感じる。
以下、凝集度のレベル。上から順に集中度が低く、問題があるとのこと。
- 偶発的凝集
- 論理的凝集
- 一時的凝集
- 手順的凝集
- 通信的凝集
- 逐次的凝集
- 機能的凝集
偶発的凝集
名前からして良くない雰囲気が伝わる。
適当(無作為)に集められたものがモジュールとなっている。モジュール内の各部分には特に関連性はない(例えば、よく使われる関数を集めたモジュールなど)。
うわあ。共通点のないコードの寄せ集めか…。
とりあえずなにかする() {
バク転をする();
ご飯を食べる(食べ物);
カバンを買う(値段) -> カバン;
}
こんなイメージだろうか。これは確かに問題ありありだ。これを動かしたときに「最終的に何をするのか」が分からない。怖すぎる。
論理的凝集
論理的に似たようなことをするものを集めたモジュール(例えば、全ての入出力ルーチンを集めたモジュールなど)。
ふむふむ。論理的、ということは、何かしらの共通点を持っているコードの寄せ集め、ということだろうか。
何かを買う() {
カバンを買う(値段) → カバン
ベッドを買う(値段) → ベッド
ボールペンを買う(値段) → ボールペン
}
何かを貰う() {
カバンを貰う() → カバン
ベッドを貰う() → ベッド
ボールペンを貰う() → ボールペン
}
「お金を払って何かを買う」や「何かを貰う」という共通の動きをするコードを集めたもの、ということか。
問題としては "何か(WHAT)" ではなく買う or 貰うという "行為(DO)" に注目しちゃってる、という点だろうか。
「"何か" は分からない、けどお金を払って何かを得る」。この文章だけでかなりの不透明さを感じるし、返却する値の型がばらつく → 複数のモデルを扱うことになるので、1:n の複雑さを受け入れてしまう形になる。
一時的凝集
動作させたときにモジュール内の各部分が時間的に近く動作する
時間的に近く動作する…?その内容は問わないのだろうか。
外出する() {
天気予報を確認する();
もし "回覧板がある" ならば {
回覧板を近所に回す(回覧板);
}
食料品を買う(お金) -> 食料品;
}
こんなイメージ?「食料品を買いに行く ついでに アレコレやっとくか」ということ?「食料品を買う」という主目的と全く関係のない行為を許してしまってるな。
「食料品を買う」という主目的の前後で関係のない行為を実行するということは、「これを実行したときに食料品以外のモデル(天気や回覧板)を操作しなければならない」という 意図しない副作用 を発生させる危険性があるのか。そして、それ自体は "論理的凝集" と同様の問題である、と。なるほどなるほど。
こういう実装をしてしまうこと、ままあるのではないだろうか。
手順的凝集
ある種の処理を行うときに動作する部分を集めたモジュール
これは分かりやすい。いわゆるルーチンやシーケンスだよね。
睡眠を取る() {
寝間着に着替える();
ベッドに入る();
掛け布団を被る();
目を閉じる();
}
これは "睡眠を取る" という挙動に関係ある動作のみに集中していて、かつ順番も決められている。「電車に乗る」ことは差し込まれないし「ついでにギターを弾く」こともしない。
問題としては「各々の動作が異なるモデルを扱っている」ということだろうか。"睡眠を取る" ことを達成するために扱うモデルとして "寝間着" 、"ベッド(マットレスと枕、掛け布団)" 、"目(体)" という 3 つに対して操作を行わなければならない。
あと、ルーチンというものは新たな工程の追加や削除を受け入れやすい。「寝る前に本を読もう」や「目を閉じたあとに羊を数えよう」だったり、「寝間着に着替えずベッドに入ってしまおう」など、"睡眠を取る" という 仕様に対しての変更性を過大に受け入れざるを得なく なってしまっている。
いや、工程を追加・削除すること自体は広義では問題ないのだが、「扱うモデルを増やす・減らす」ということが実装カロリーを高くしてしまっている。それ自体は "論理的凝集" や "一時的凝集" と同じ問題ではある。
そして、こういった実装は結構見かける。
通信的凝集
同じデータを扱う部分を集めたモジュール
"通信的" という言葉が枕詞になっている意味はあまり分からない。
部屋を掃除する() {
ゴミ箱を空にする();
床に掃除機をかける();
床を雑巾で拭く();
机の上を片付ける();
}
というイメージ? "部屋(にあるもの)" というモデルに対する動作のみで構成されている。
"手順的凝集" との違いは 行為の順序を問わない こと。机の上を先に片付けてもいいし、まず床を掃除してもいい。
特徴としては「作用を受けるモデルは 1 種類のみ」ということと「記述されている行為はお互いに影響を及ぼさない」こと。何かができなければ達成されないということはない。ゴミ箱を無理矢理空にしなくてもいいし、床を雑巾で拭かなくてもいいのだ。
ただし「個々の行為の達成に関して興味を持たない」ということは、「その関数は最終的に何を実行したのか」という点を曖昧にし、外部に対して 何をして何をしていないのかを隠蔽してしまう 危険性がある。それは正しく「子供に夏休みの宿題の進捗を尋ねる親」そのものだ。
逐次的凝集
ある部分の出力が別の部分の入力となるような部分を集めたモジュール
なるほど、出力をバケツリレーする感じかな。だとすると、行為の順序は定められていそう。
料理を作る(食材) -> 料理 {
食材に下処理を施す(食材) -> 加工済み食材;
食材を調理する(加工済み食材) -> 料理;
}
こんな感じにとあるモデル(食材)がお互いの入力と出力になっていて、この順番でないと正しく動かない。
"手順的凝集" と決定的に異なるのは「扱うモデルが関連するもののみに限定される」こと。関係のないモデルを扱ってしまうと、"手順的凝集" となってしまう。
機能的凝集
これが最後。責任分担への集中度が一番高いらしい。
単一のうまく定義されたタスクを実現するモジュール
つまり、たった一つのことのみに集中している、ということか。
商品を買う(お金) -> 商品 {
商品の対価を支払う(お金) -> 商品;
}
流石に集中度が高すぎるイメージになったけど、こんな感じか。末端のモジュールに関してはこれで問題なさそう。
コアなロジックでこれを遵守するのは難しそう。頑張って "逐次的凝集" だろうけど、それでも十分にメンテナンス性は担保されるかなー。