<クリーンコード設計>モジュール結合度ってどう意識すれば良いの?

プログラミング

はじめに

プログラムというものはロジックが正しければ、どんな書き方であったとしても、ひとまずは動いてしまうのである。
このため、この世にはとても保守できないプログラムで溢れており、後でそのコードを拡張したり、保守したりするプログラマを困らせている。
「モジュール結合度」を意識するだけで、保守性はだいぶ向上する。
とはいえ、これがなかなか難しく、「知ってるけど、コードに反映できない」という方もいるのではないだろうか。
実際、何十年も経験あるベテランエンジニアでも、モジュールが超密結合なコーディングしているケースもよく見受けられる。
そこで今回は、モジュール結合度をどう紐解いて、どのようにコーディングに反映させていくと保守性が高いプログラムとなるかを解説していこうと思う。

モジュール結合度とは?

他のモジュールとの結びつきの強さのことを示す。
一般的には、モジュール結合度が強いほど、悪いプログラムとされ、結合度が弱いほど、良いプログラムとされる。
結合度が強すぎることのデメリットとしては、以下のようなことが挙げられる。

  • 関連モジュールを変更・拡張する敷居が高くなる
    例えば、AモジュールがBモジュールに強く依存していたとき、Bモジュールを変更したことによってAモジュールも変更を加える必要が出てくる。
    それが複数モジュールにもまたがると、バグの温床となりやすく、またテストもその分だけ必要になる。
  • 単体テストの敷居が高くなる
    単体テストをする際、そのモジュールが依存している他のモジュールの状態も気にしなければならない。
    依存する外部状態の数だけ、マトリクスが増え、テストケースが倍々になる。
    網羅性を必要とする単体テストにおいて致命的である。
  • 読みにくいプログラムになる
    完全に独立しているモジュールは、そのモジュールを見るだけでどういった処理をしているか理解できる。
    ただ、外部と密接したモジュールは、外部を意識しないと処理内容が理解できなかったりする。
    プログラムの保守が大変になる。

以上のことから、モジュール結合度はなるべく意識してプログラムすべきである。
たまに、速度とのトレードオフでは?という人がいるが、とんでもない。
仕様変更するときの工数倍増、単体テストの工数倍増、他の人に機能を引き継ぐときのリスク倍増により、速度も大幅に落ちる可能性が高いと思う。

続いて、モジュール結合度の一覧は以下のようになり、上から結合度が弱い(良い)⇒強い(悪い)順に並べている。

結合説明
データ結合単純なデータ(整数、文字列等)のみの受け渡しを行う。
スタンプ結合複合データ(構造体、オブジェクト等)を含めたデータの受け渡しを行う。
制御結合モジュール内でどの処理を行うかを判定するための情報(フラグ等)を含めたデータの受け渡しを行う。
外部結合外部ライブラリや外部デバイスインタフェースを複数モジュールで共有する。
共通結合グローバルなデータ(グローバル変数等)を複数モジュールで共有する。
内容結合別モジュールの内部実装に依存する。

では、全てデータ結合となるように実装すればよいのか?
それは不可能である。
適材適所でベストな実装を行い、最低限結合度が小さくなるように努めていくことが望ましい。
これから、各結合におけるポイント、何が許されて何が許されないのか等の要点を記載していく。

データ結合/スタンプ結合

# データ結合
def get_self_introduction_text(fullname, age, _from):
    return f'私は{fullname}です。'+\
    f'年齢は{age}です。' +\
    f'出身地は{_from}です。'
# return : 私は田中太郎です。年齢は23です。出身地は東京都です。
class Profile:
    # コンストラクタ    
     def __init__(self, fullname, age, _from):
        self.fullname = fullname
        self.age = age
        self._from = _from

# スタンプ結合
def get_self_introduction_text(profile):
    return f'私は{profile.fullname}です。'+\
    f'年齢は{profile.age}です。' +\
    f'出身地は{profile._from}です。'

# return : 私は田中太郎です。年齢は23です。出身地は東京都です。

上記のようにデータ結合では単純データ、スタンプ結合では複合データを受け渡す。
できる限りこのような実装にすべきであるが、さすがに難しいので、最低限このような実装にする必要があるスコープを記載する。
イメージとしては、コンポーネント、部品、共通関数と言われるようなプログラムの末端に位置するモジュールである。
最も再利用性が高い書き方であるため、上記のような汎用的な処理を行ったり、使い回したりするモジュールは、最低限このように実装すべきである。
データ結合となるものが他のプログラムでも使いまわせるようなモジュールが多いのに対し、スタンプ結合となるものはそのプログラム独自の特徴が少しあったり、複雑性が少し高かったりする。

これらで実装すべきモジュールの例としては、以下が挙げられる。

  • 演算を行うモジュール
  • 文字列変換を行うモジュール
  • データ構造を生成するモジュール
  • データの登録/更新/削除を行うモジュール

など。

制御結合

class Profile:

    # コンストラクタ    
     def __init__(self, fullname, age, _from):
        self.fullname = fullname
        self.age = age
        self._from = _from

# 制御結合
def create_introduction_by_type(profile, _type):
    if _type == 'フランク' :
        # フランクなintroduction処理
    elif _type == 'オフィシャル' :
        # オフィシャルなintroduction処理

上記の例では、「_type」によってモジュール内部の処理が分岐している。
この「処理の分岐」のキーを外部からもらう、というところが独立性を低くしているのだが、この書き方はあるスコープにおいては、全然あってもよいものだと思う。
そのスコープというのは、各処理部への入り口を制御するような、mainと各処理部の間に位置するようなモジュール、比較的プログラムの上位に位置するこのモジュールにおいてはこの結合は全然OKである。

外部結合/共通結合

class Profile:

    # コンストラクタ    
     def __init__(self, fullname, age, _from):
        self.fullname = fullname
        self.age = age
        self._from = _from

sample_profile = Profile('田中太郎', '23', '東京都')

# 共通結合
def input_fullname():
    print(f'名前を入力してください。\nサンプル:{sample_profile.fullname}')
    # 名前入力処理

# 共通結合    
def input_age():
    print(f'年齢を入力してください。\nサンプル:{sample_profile.age}')
    # 年齢入力処理

直感で分かるかもしれないが、グローバル変数を共有しているほど、結合度は強い。
イメージとしては、プログラムの階層が下りてくほど、使いたくない。
ただし、main関数、または各処理部において処理フローを制御するコントロール系のモジュールのような、一般的にプログラムの「台所」と呼ばれるモジュールでは、たくさん登場しても問題ない。
むしろ、ここに凝縮させることによって本当に独立が必要なモジュールに影響を与えない作りにすることが必要である。

この辺になってくるとモジュール結合度が強くなってくるが、「使うな」というわけではない。
使っていいスコープと使うのが好ましくないスコープがある、ということであり、これを正しく使い分けたプログラムが良いプログラムなのである。

内容結合

これはほとんどの高級プログラミング言語では再現できない。
言語仕様としてできないようになっている。
他のモジュールの内部実装に依存する例として、C言語の例を記載する。
いきなり言語が変わって申し訳ない。
大体の言語は再現できないのだ。

#include <stdio.h>
#include <string.h>

typedef struct {
    char fullname[10];
    int age;
    char from[10];
} PROFILE;

// 内容結合
// 年齢を出力する関数
void printAge(void* agePointer) {
    // ポインタからageを取得
    int age = *(int *)(agePointer);
    
    // せっかくだからfromがアメリカの人には、英語で表示してあげよ。
    // fromは、ageの後の領域に入ってるはずだ!
    char* fromPointer = (char*)(agePointer + sizeof(int));

    // fromが日本なら日本語で
    if (strcmp(fromPointer, "America") == 0) printf("I am %d" , age);
    // fromがアメリカなら英語で
    else if (strcmp(fromPointer, "Japan") == 0) printf("私は%d才です。", age);
    
    return;
}

int main(void){
    PROFILE profile = {"田中太郎", 23, "Japan"};
    // 年齢表示    
    printAge(&profile.age);
    return 0;
}

// result : 私は23才です。

C言語のポインタを用いて再現した。
親切な関数ではあるが、バリバリ呼び出し元の処理に依存している。
age、fromの順番が変わったらロジックから見直さなければならない。
これが結合度MAXのコーディングである。
この結合はたとえC言語であっても、やってはならない。これに関しては「使うな」である。

最後に

今回はモジュール結合度について、その説明と実際のコーディングでどのように意識すればよいのか?を解説した。
保守性の高いプログラムを書くことは、プログラマとして非常に重要なことである。
今後も、クリーンコードに関する記事を書いていこうと思う。

コメント

タイトルとURLをコピーしました