存在感の薄い「凝集度」
品質の高いソフトウェアにするために「結合度が低く、凝集度が高い」設計がよいとされています。ソフトウェアのコンポーネント(関数、クラス、モジュール etc)をどう分割するか、またどうやって互いに関連付けるかに関する設計方針ですね。この「結合度」、「凝集度」の意味は一言で言うと以下のようになります。
- 結合度 - コンポーネント間の依存の多さ、また依存の度合い(低いほど良い)
- 凝集度 - 関連する操作が一つのコンポーネント内にまとまっている度合い(高いほど良い)
結合度と凝集度は表裏一体の概念です。コンポーネントを上手に分割できれいれば、だいたい結合度は低くなるし凝集度は高くなります。関連する操作が一つのコンポーネント内にぎゅっと押し込められていれば、関連の薄い操作は別コンポーネントにあって互いに依存しないようになっているし、逆にコンポーネント間の依存が少なければ、各コンポーネントの責務が明確で互いに関連する操作がコンポーネントで完結している、みたいな感じですね。
「結合度」はよく話題になりますし、コンポーネント間を疎結合に保つためのテクニックもいろいろ知られています。DI(依存性注入)とか依存関係逆転の原則とかAPIファーストとかですね。
でも、「凝集度」のほうは存在感薄くないですか?結合度の片割れのわりには全然話題にならないし、凝集度を高く保つためのテクニックもあまり知られていません。
Qiitaの記事数で比べると人気の差は歴然としています。キーワード検索の検索結果は「結合度」が3807件ですが、「凝集度」は234件しかありません(2020年9月調べ)。10分の1以下。凝集度ちゃん人気なくてカワイソウ。
で、そもそも凝集度という概念がわかりにくいと思うんですよね。関連する操作を一つのコンポーネントにまとめるとか、コンポーネントの責務を明確にして一つのコンポーネントが一つの責任に集中するように設計するとか、言っていることはわかるような気がしますが、具体的にどうすればいいかピンと来ませんよね。
そこで次の節では、凝集度を測るメトリック LCOM を題材に、凝集度をより具体的に理解していきたいと思います。
凝集度のメトリック LCOM
クラスの凝集度を測るメトリックに LCOM (Lack of Cohesion of Methods)というものがあります。直訳すると「メソッドの凝集度の不足度」です。その名のとおり、凝集度が足りないと値が大きくなります。LCOM にはいくつかのバリエーションがありますが、今回紹介するのはいわゆる LCOM4 と呼ばれているものです。
高凝集なクラスは、クラス内のメソッドが互いに関連しているはずです。この「互いに関連している」を定量的に評価しようとするのが LCOM というメトリックです。
LCOM はメソッド内で参照しているインスタンス変数(と他のメソッド)に着目します。直感的には、2つのメソッドが互いに関連しているならば、きっと同じインスタンス変数を参照しているはずです。LCOM ではそういう互いに関連する(=同じインスタンス変数を直接的・間接的に参照している)メソッドでグループ分けして、そのグループの数を数えます。
図で表すとわかりやすいです。あるクラス内のメソッドが次のような構造になっているとします。
このクラスには4つのメソッドと3つのインスタンス変数があります。methodA は methodC を呼び出し、methodC は varA と varB を参照していて...のように、矢印は参照関係を表しています。矢印でつながっているメソッド同士を一つのグループにまとめると、青い枠で囲まれたグループが2つあります。グループ数がそのまま LCOM の値になります。このクラスの LCOM は 2 です。
LCOM4 の意味は明快で、次のようなものです。
- LCOM = 1 なら凝集度が高いクラス。これで OK
- LCOM > 1 なら凝集度が低いクラス。クラスを分割するようリファクタリングを検討すべき
上記のクラスについて言えば、LCOM = 2 なのでこれは凝集度が低いクラスで、methodD を別クラスにできるか検討しようということになります。
LCOM4 の形式的定義
LCOM4 のざっくりとした説明は前節のとおりですが、一応形式的な定義も見ておきましょう。
【LCOM4 の定義】
あるクラスにおいて、M
をメソッドの集合、I
をインスタンス変数の集合とします。次のような無向グラフ G
を考えます。
- 頂点の集合は
M
- 辺の集合は
{ <m, n>∈MxM | (mがiにアクセスし、nがiにアクセスするようなi∈Iが存在する)or(mがnを呼び出す)or(nがmを呼び出す) }
このとき、LCOM の値はグラフ G
の連結成分の個数です。
ちなみにですが、先に述べたように LCOM にはいくつかバリエーションがあります。凝集度 - Qiita に LCOM1 〜 3 までの定義がありますので、参考までに。
JavaScript の実装例
せっかくなので、JavaScript のクラスで実装例を見ておきましょう。
意味のあるクラスではありませんが、次のクラスは LCOM = 1 の例です。
// LCOM = 1 (高凝集)
class A {
methodA() {
return this.var1
}
methodB() {
return this.var1
}
}
次の例も LCOM = 1 です。
// LCOM = 1
class A {
methodA() {}
methodB() {}
methodC() {
this.methodA()
this.methodB()
}
}
次のクラスは LCOM = 2 です。
// LCOM4 = 2 (低凝集)
class A {
methodA() {
return this.var1
}
methodB() {
return this.var2
}
methodC() {
return this.methodB()
}
}
ESLint のルールを実装してみた
勉強がてら ESLint プラグインを作ってみました。JavaScript クラスの LCOM4 の値を計測して 2 以上だったら警告を出してくれます。
実用的ではないと思いますが、参考までに。
まとめ
今回は「結合度」と比べて影の薄い「凝集度」にスポットを当てて説明してみました。
クラスの凝集度を測定するメトリック LCOM の紹介をしましたが、実際には凝集度はクラスに限定した概念ではなくもっと幅広いものです。結合度は関数、クラス、モジュール、マイクロサービスなど至るところに現れますが、同様に、凝集度もさまざまなレベルで考えることができます。
凝集度のメトリックもさまざまなものが考案されています。興味があれば調べてみるのをおすすめします。