#はじめに
良い設計をしていくにあたり「結合度」や「凝集度」といった基準があります。
プログラムを書くにあたり、必ず知っておくべき基本的な設計原則です。
今回はその中でも「結合度」について説明していきたいと思います。
コードはC++で書いていますが、関数だけなのでどなたでも読めると思います。
#結合度とは
利用者またはメンテナンスをする者にとって対象を利用、保守しやすいように対象の内容が整理、分割できているかどうかを、その状態によって段階に分けて表現するソフトウェア測定法の一種です。 (Wikipedia引用)
#結合度の種類
結合度は7段階のレベルで表すことが出来ます。
レベル | 名前 | 内容 |
---|---|---|
1 | メッセージ結合 | 引数のないメソッドの呼び出し |
2 | データ結合 | 単純なデータの受け渡しのみを行う |
3 | スタンプ結合 | 複数のモジュールが複合データ構造を共有し、その一部のみを使用する |
4 | 制御結合 | 他のモジュールを制御するフラグを受け渡している |
5 | ハイブリッド結合 | 値の範囲により意味が異なったり、状況により意味が異なるデータがある |
6 | 共通結合 | グローバルデータで、モジュール間の受け渡しをしている |
7 | 内容結合 | 他のモジュールを直接参照している |
1が最も結合度が低く、良い結合と言える。
数字が大きくなるほど結合度が高くなり、悪い結合となる。
良し悪しをはっきり区切ると
良い結合 は
1 - メッセージ結合
2 - データ結合
3 - スタンプ結合
悪い結合 は4~7
4 - 制御結合
5 - ハイブリッド結合
6 - 共通結合
7 - 内容結合
それぞれについて詳しく説明していきます。
##1. メッセージ結合
引数のないメソッドの呼び出しです。
データのやり取りはなく、メッセージのやり取りということでメッセージ結合と呼ばれてるそうです。
void hello(){
printf("Hello World\n");
}
もちろん戻り値がvoid以外でも大丈夫です。
むしろ、void以外の方が多いです。
メッセージ結合は、省略されて以下の6段階で表されることも多いです。
レベル | 名称 |
---|---|
1 | データ結合 |
2 | スタンプ結合 |
3 | 制御結合 |
4 | ハイブリッド結合 |
5 | 共通結合 |
6 | 内容結合 |
##2. データ結合
単純なデータの受け渡しのみを行います。
単一のデータとは、引数が1つという意味ではなく、まとまりのあるデータのことを言います。
意味のある構造体とは、メンバ変数たちがそれぞれ関係している(まとまりのある状態変数)構造体のことです。
データ結合では、モジュール内で構造体のすべてのメンバ変数が使われていることが理想的です。
以下に例を示します。
struct Point {
int x;
int y;
};
int func1(int x, int y){
return x + y;
}
int func2(Point p){
return p.x + p.y;
}
このような func1 と func2 はどちらも受け取った引数がすべて使われています。かつ x と y は二次元座標と考えればまとまりのあるデータと言えるのでデータ結合となっています。
##3. スタンプ結合
複数のモジュールが複合データ構造を共有し、その一部のみを使用している結合です。
ここまでが良い結合です。
struct Person {
int age;
double height; // 身長
double weight; // 体重
double bmi;
string occupation
};
double func(Person p){
return p.bmi;
}
上の例は少しやりすぎですが、イメージは構造体で渡しているのにメンバ変数は一部しか使われていないので、無駄な結合(依存)が生じてしまいます。
出来るだけ使う変数だけを引数に渡してやるのが良いでしょう。
##4. 制御結合
ここから悪い結合になります。
他のモジュールを制御するフラグを受け渡している結合です。
void func(int flag){
switch(flag){
case 0:
// ユーザー情報の初期化処理
break;
case 1:
// ユーザー情報の書き込み処理
break;
case 2:
// .....
break;
default:
// .....
break;
}
}
フラグで関数の処理を制御している関数です。
これはやってしまいがちですがやめましょう。
新しく機能を追加する度に、呼ばれる側の関数にcase分が増えて肥大化してしまいます。
また、使う人フラグに渡す値(関数の中身)を知らないと使うことができないという問題もあります。
こういった場合の解消方法は、func関数に入る前に必ずどこかでflagの値を分岐させて代入しているはずです。フラグに代入するのではなく直接関数を呼ぶことで解消できるでしょう。
##5. ハイブリッド結合
値の範囲により意味が異なったり、状況により意味が異なるデータがある結合です。
ハイブリッドデータとは「値によって意味が異なるもの」や「ビットごとに意味が異なる」データのことです。
「値によって意味が異なるもの」の例を示します。
int func1(){
// 何らかの処理
return ret;
}
この関数の返り値retは以下のように表される。
- 正の値(ret >= 0) 正常値
- 負の値(ret == -1) エラー1
- 負の値(ret == -2) エラー2
このように1つの変数に複数の関心事扱うと、その変数に複数の関心事を扱うモジュールが依存してしまいます。
次に「ビットごとに意味が異なる」の例を示します。
void func2(int n){
int x = n & 0xff00;
int y = n & 0x00ff;
// 何らかの処理
}
引数 n は、このfunc2関数では以下のように扱う。
- 上位8ビットは x
- 下位8ビットは y
このようにビットごとに意味が違うものは結合度は悪いです。
ビットは内容の理解が難しく、間違ったデータを取り出してしまったりする可能性もあります。
また、int型は16ビットであったり、32ビットの可能性もあります。つまり、ハードウェアに依存してしまい移植したりすることが出来なくなってしまいます。
##6. 共通結合
グローバルデータで、モジュール間の受け渡しを行います。
グローバル変数のことです。Javaなどの言語ではstatic変数のことです。
グローバルデータは他のモジュールで書き換えられてしまう可能性があります。
また、グローバルデータ定義の変更がそれを使用しているモジュール全てに影響してしまいます。
グローバルデータは便利ですが、やめるべきです。
##7. 内容結合
自身の所有するデータが、他のモジュールかた直接操作される状態の結合です。
これは、最近では使われなくなったgoto文やアセンブラでしか使われないので、あまり意識する必要はないかもしれないです。
#まとめ
モジュール同士の結合度が低いほどコードの可読性や再利用性が高まり、障害が発生した場合も対応がしやすくなります。
コードを書く際やレビューをする際などは
- メッセージ結合
- データ結合
- スタンプ結合
を意識して書いていくことを強くオススメします。