凝集度と結合度:このコードのどこが悪いのか?初めてのソフトウェアメトリクス(中編)

» 2005年10月07日 12時00分 公開
[長瀬嘉秀, 西田高士テクノロジックアート]

 前編「ソフトウェアの品質を数値化して確かめる」では結合度について少し触れましたが、今回は結合度とともにソフトウェア設計において古くから知られている凝集度についても紹介し、ソフトウェアメトリクスの解説をしていきます。

 抽象的な話だけになってしまうと、具体的なイメージがつかみにくいので、実際のプログラムコードを示し、何が良くて、何が悪いのかを明確にしていきます。理論的な話も重要ですが、メトリクスの測定が実際にどう評価されるかを理解して、良いプログラムを作れるようになる手助けになれば幸いです。

 それでは、メトリクスの詳細を見ていきます。

1. 凝集度と結合度

1.1. 凝集度とは?

  凝集度とは、クラスやパッケージ内の機能要素と情報要素間の関連性の強さを表す指標です。互いに関連する機能や情報があちこちに分散していると、仕様変更が生じた場合の影響範囲が広くなってしまいます。これらの機能や情報が局所化されている、つまりは閉じたモジュール内に収まっていることが望ましいわけです。そのため、基本的には凝集度は高いほど良い設計といえます。

 凝集度を意識することにより、関連性の高い機能が細分化され、保守性や理解のしやすさを改善することができます。また、結合度も同時に促進されることが多いといわれます。

1.2. 結合度とは?

 前回の記事でも取り上げましたが、結合度とは、クラスやパッケージ間で、呼び出し関係にあるメソッドの結び付きの強さを表す指標です。結合度が高くなると複数のクラスやパッケージ間で依存度が上がってしまうため、保守やメンテナンス、仕様変更などの対応がしづらくなります。また、設計的な観点からしても、複雑で分かりづらいものになってしまいます。

 結合度を意識することにより、保守性やプログラム構造および設計の理解のしやすさを改善することができます。既存のシステムに対しては、結合度が高くなっているクラスやパッケージ間に着目し、数値を低くするように調整を行えると、結果的に保守性や設計の改善を行うことができるでしょう。

2. 凝集度の測定方法

 凝集度の測定とは、クラスやパッケージ内の機能要素と情報要素間の関連性の高さを測るものです。オブジェクト指向言語において、着目しているクラスに、互いに関連するメンバ変数とメソッドが十分に集まっている、つまり局所化されているのであれば、そのクラスのメンバ変数1つ当たりへアクセスするメソッドの数は多くなります。その点に着目したメトリクスの1つがLCOM*(Lack of Cohesion in Methods)です。

 LCOM*は次の式で定義されます。

ALT 図1 LCOM*の式

 ここで、

Ajは、着目しているクラスのj番目のメンバ変数

aは、着目しているクラスのメンバ変数の個数

mは、着目しているクラスのメソッドの個数

μ(Aj)は、メンバ変数Ajにアクセスしているメソッドの個数

を表しています。

 簡単にいってしまうと、メンバ変数1つ当たりへアクセスするメソッドの数が少ないとLCOM*は1へ近づき、多ければLCOM*は0へ近づきます。つまり、LCOM*の値が小さいほど凝集度が高いといえます。

 話を具体的にするためにJavaを想定して説明しましょう。次のようなクラスを考えます。

class SampleClass {
    private int a1, a2, a3, a4;
    public void m1() {
        a1=0; a2=1;
    }
    public void m2() {
        a1=1; a2=2;
    }
    public void m3() {
        a3=0; a4=3;
    }
    public void m4() {
        a3=1; a4=2;
    }
}
コード1 Javaを想定したクラス

 このとき、LCOM*の計算式に登場する各項の値は次のとおりです。

a=|A|=メンバ変数の数=4

m=|M|=メソッドの数=4

μ(a1)=a1にアクセスするメソッドの数=2

μ(a2)=a2にアクセスするメソッドの数=2

μ(a3)=a3にアクセスするメソッドの数=2

μ(a4)=a4にアクセスするメソッドの数=2

 よって、このクラスのLCOM*は、

LCOM*=(((1/4) * (2+2+2+2))ー4)/(1 ? 4)=0.666……

となります。

 ここでよく見てみると、{a1, a2, m1, m2}のグループと{a3, a4, m3, m4}のグループは互いに何の関係もないことが分かります。ということは、これらのグループごとにクラスを分けることによって、それぞれのグループの局所化が進み、凝集度が向上するはずです。これらのグループごとにクラスを分割した場合のLCOM*は、それぞれ次のようになります。

クラス1のLCOM*=0

クラス2のLCOM*=0

 前編で紹介したメトリクス測定ツールの1つである「Eclipse Metrics Plugin」を使って、上の例を測定した結果は、次のようになります。

ALT 図2 クラス分割前の凝集度測定結果(凝集度の測定値が67と高いため、グラフは赤、下の表にも数値が赤で表示される) クリック >> 拡大
ALT 図3 クラス分割後の凝集度測定結果(クラス分割後、凝集度の測定結果が0となり、グラフ、表の値ともにブルーになる) クリック >> 拡大

 上記は極端な例ですが、LCOM*がどのように凝集度をとらえているのかお分かりいただけたと思います。このように、LCOM*とは、メンバ変数を情報要素、メソッドを機能要素と見なして、それらの相関性を測定する方法です。

 なお、LCOM*について、さらに詳しく知りたい方は、下記の参考文献などをご覧ください。


▼参考文献 B.Henderson-Sellers, "Object-oriented metrics : measures of complexity", 1996


3. 結合度の測定方法

 結合度の測定方法とは、クラスの中に定義されている型の種類の数を数えます。具体的には、継承クラス、インターフェイス、メンバ属性、メソッドのパラメータなどです。型の数が多いとほかのクラスやパッケージ間の依存関係が分散し過ぎていると判断できます。

 パッケージ間での結合度を測る場合は、パッケージ単位で出現するすべての型の種類を数えます。話を具体的にするためにJavaを想定して説明しましょう。以下のソースは、座席予約システムの予約クラス(Reservationクラス)です。今回のテーマに必要な部分だけを抜き出しています。

public class Reservation {
    /** 最後に受け付けた受付番号 */
    private static int lastReceiptNumber;
    /** 座席管理 */
    private SeatManager seatManager;
    /**
    * 座席を予約します。
    * @return 受付番号を返します。
    */
    public int reserve(Customer customer){
    this.seatManager.reserve(customer); ・・・・・・・(1)
    return newReceiptNumber();
    }
}
コード2 改善前のソース

 1つのクラスのソースを見ただけでは、ほかのクラスとの関連が分かりづらいので、以下にクラス図を記します。

ALT 図4 改善前のクラス図

 予約クラス(Reservation)はメンバ変数として、座席管理クラス(SeatManager)を持っていますので、予約クラスと座席管理クラスとの間に関連があります。また、reserve()メソッドで、 顧客クラス(Customer)を受け取る仕様になっているので、顧客クラスとも関連があります。

 とてもシンプルな構成ですが、改善の余地があります。座席を予約するためには、顧客オブジェクトが必要ですが、予約クラスにとって本当に必要でしょうか。予約クラスはパラメータとして受け取った顧客オブジェクトに対して何の操作もせずに、単に座席管理クラスに引き渡しています(ソースコード中(1))。座席管理クラスに必要であっても予約クラスでは何も使っていません。必要ではないのです。予約処理に必要なのは、「誰が」座席を予約するかということであり、顧客をユニークにする顧客番号だけでも良さそうです。それでは、予約クラスのreserve()メソッドのパラメータを、顧客番号に変更してみます。

public class Reservation {
    /** 最後に受け付けた受付番号 */
    private static int lastReceiptNumber;
    /** 座席管理 */
    private SeatManager seatManager;
    /**
    * 座席を予約します。
    * @return 受付番号を返します。
    */
    public int reserve(int customerNumber){
        this.seatManager.reserve(customerNumber);
        return newReceiptNumber();
    }
}
コード3 改善後のソース

ALT 図5 改善後のクラス図

 予約クラスは顧客クラスを知る必要はなく、顧客番号さえ渡してもらえば、予約処理をすることができるようになりました。今回の例を見ただけでは設計の改善効果が分かりづらいのですが、普段の開発現場の場合、複数のクラス間の関連があるのではないでしょうか? 複数の関連があると、ほかのクラスとの依存度が上がるので、後々の保守や拡張に影響してしまいます。

 「Eclipse Metrics Plugin」を使って、改善前後を測定した結果は、次のようになります。

ALT 図6 あるプログラムの結合度測定結果(上図の各クラスの測定結果は、Reservationクラスが4、Customerクラスが3、SeatManagerクラスが2となっている) クリック >> 拡大
ALT 図7 あるプログラムの結合度測定結果( 修正後、結合度が低くなり、Customer、Reservationクラスの測定結果が3、SeatManagerクラスの測定結果が2となる) クリック >> 拡大

4. 測定結果の分析から

 今回紹介した凝集度と結合度はそれぞれ異なる概念ですが、実際には相関性を持っています。凝集度を高める際に、着目しているモジュールの外部まで目を向けて、関連する情報要素や機能要素をモジュール間で移動すると、結果的に結合度が低くなることが多々あります。実際の開発現場での改善作業では、このようにモジュール間をまたがる修正が多いのではないでしょうか?

 次回は、メトリクスを利用した品質改善の実践について、詳しく説明します。

Copyright © ITmedia, Inc. All Rights Reserved.