はじめに
どうも、すいみーです。
凝集度と結合度。
今まで言葉こそ知っているものの正直何なのかよく分かってないなという背景がありました。
特定の言語に依存する話ではなく横展開できる知識なので、どこかのタイミングでちゃんと理解しておきたいなと思っていましたので、まとめていきます。
具体的には、それぞれの概念が何なのかとどんなメリデメがあるのかの二点をまとめる形になります。
※プリンシパルオブプログラミングの内容をベースに学習しています。
※サンプルコードはKotlinぽい疑似言語で紹介しています。
個人的な勘違い
良くモジュールの凝集度がうんたらかんたらという話を聞くので、マルチモジュール単位の話かと思ってました。
各参考文献の具体例を見ると一つの関数内の処理同士の関連だったので、単位は下記の様にまちまちであると理解しました。
- パッケージ
- クラス
- 関数
凝集度とは
一つのモジュール内にある処理と処理の間にどのような関係性があるのか?を表した概念だと捉えました。
凝集度は、下記の様にいくつかの種類に分類されています。
- 暗黙的強度
- 論理的強度
- 時間的強度
- 手段的強度
- 連絡的強度
- 情報的強度
- 機能的強度
下に行くほど良い凝集度とされます。
早速、凝集度をいくつか見ていきたいなと思います。
暗黙的凝集
これは、処理Aと処理Bの間に特に関連性がない状態です。
fun main() {
openDialog()
deleteFile()
}
ダイアログを展開しているのとファイルを削除する処理が一つのメソッド内にあります。
この場合二つの処理は無関係です。
こうした処理の集まりであるモジュールを暗黙的凝集と呼びます。
これは最もレベルの低い凝集度であり、避けるべきものとして良く紹介されます。
何が良くないの
-
命名が難しくなる
かなり抽象的な名前になり、内部で行われている処理を的確に表現しにくく成ります。
つまり、その関数を見ても何をしているのかが分かりにくいという点で可読性が良くないです。 -
変更の影響範囲が広がる
関係のない処理が一つのモジュール内に集中していると、変更の理由が多岐にわたります。
一つの呼び出し元都合で変更したものが、他の呼び出し元にとっては望ましくない場合があります。
処理同士の結びつきが全くないと、逆にモジュール同士の結合が強くなり変更による影響範囲が広がってしまいます。
論理的強度
これは、処理Aと処理Bが抽象的に同じような事をしている状態です。
fun main() {
/.../
when(number) {
0 -> processA()
1 -> processB()
}
}
何が良くなったの?
こちらも暗黙的凝集と同様に処理同士の関連性がないですが、抽象的に関連しているので共通化のチャンスがあります。
特定の動作が局所化されるので、暗黙的強度と比較して変更の影響範囲を抑えられる。
fun processAorB(number: Int) {
when(number) {
0 -> processA()
1 -> processB()
}
}
しかし、処理同士はまだ関連性がないのでまだ良いコードとは言い切れません。
例えば、今後出し分けの数が増えるとすると命名が難しくなったり、読みにくくなったり(可読性の低下)、再利用性が低くなったりします。
時間的強度
処理同士が同じタイミングで実行される状態です。
代表的なのが初期化処理です。
fun setupMain() {
hogeInit()
fugaInit()
}
論理的強度と同様に抽象的に見て同じような事をしているので、可読性が一段上がります。
また、呼び出される場所が変わることが少ないため、変更時の影響範囲をより抑えられたり、再利用性が上がります。
手順的強度
処理同士が順番的に前後しています。
ここで、初めて処理同士が連続的な関連性を持ちます。
まず、下記のように特定の機能において三つのフローチャート的なサブ機能があるとします。
サブ機能のそれぞれは、順番が決まっています。
fun hogeFeature {
partA()
partB()
}
この時、partAとpartBは手段的強度に値します。
この二つの処理は、順番を入れ替える事が出来ないので、時間的強度よりも強度が強いです。
上から読み下す時に、意味が連続しているので可読性も上がっています。
しかし、partBのみを使いたい場合を考えると再利用性において問題があります。
内部で出し分けをすると論理的強度になってしまうので、凝集度のレベルが下がってしまいます。
例えば、エラーやnullチェックなど。
連絡的強度
扱うデータが同じであるという点で処理同士が関連性を持っています。
先程のhogeFeatureと比較してモジュール独立性が増しています。
かなり抽象的な具体例なので分かりにくいですが、手段的強度と比較してpartBを単独で使いたというケースが少なくなります。
独立性が増すとモジュール自体の再利用性が高まります。
fun hogeFeature(data: Data) {
processA(data)
processB(data)
}
情報的強度
ここは、処理の出力がもう一方の処理の入力になっている状態です。
fun hogeFeature() {
val result = processA()
processB(result)
}
機能的強度
処理同士が一つの機能の部分である状態です。
上記と同じように見えますが、処理同士は独立しておらず再利用性がない状態です。
ただ、それが故に単一モジュールを機能させるために活用される処理同士です。
特殊な計算などで、そのモジュールに特化した処理などが具体例として挙げられますが、かなり理想的です。
fun hogeFeature() {
val result = partA()
partB(result)
}
感想
具体例がやや抽象的になりましたが、概念自体は分かったかなと思います。
あとは、読者自身が得意とするプラットフォーム・言語に当てはめて想起してみてください。
参考