Help us understand the problem. What is going on with this article?

モジュール間結合度についてPythonのコード付きでまとめてみる

目的

基本情報で勉強して以来あまり触れて無くて、当時はコードを書いてみたりはしていなかったので改めて勉強として。

モジュール間結合度とは?

モジュールとモジュールの関連がどれぐらい強いかを示しています。
モジュール間結合度が低くなるようにすることで、モジュールの独立性を高めることができるとされています。
モジュールの独立性が高まると、以下のようなメリットがあります。

  • モジュールが再利用しやすくなる
  • テストがしやすくなる
  • 仕様変更時の影響範囲を狭めることができる

例えば、以下のコードを見てみましょう。
この関数は現在の西暦をもとに、うるう年かどうか判断をしています。

モジュール独立性の低いコードは再利用性も低い

from datetime import datetime
from calendar import isleap

#今年がうるう年かどうか判断し、結果を出力する関数
def A():
    this_year = datetime.now().year
    if isleap(this_year):
        print('今年はうるう年です')
    else:
        print('今年はうるう年ではありません')

では、このモジュールに「10年前がうるう年だったか判断する仕組み」を取り入れたくなったとします。

from datetime import datetime
from calendar import isleap

#今年がうるう年かどうか判断し、結果を出力する関数
def A():
    this_year = datetime.now().year
    if isleap(this_year):
        print('今年はうるう年です')
    else:
        print('今年はうるう年ではありません')

#10年前がうるう年かどうか判断し、結果を出力する関数
def B():
    this_year = datetime.now().year - 10
    if isleap(this_year):
        print('10年前はうるう年です')
    else:
        print('10年前はうるう年ではありません')

モジュール独立性が低いコードはテストしにくい

ほぼ同じようなコードが書かれており、そもそも直感的にあまり良い気がしないでしょう。
さらに、テストをするときにも問題があります。
仮に関数Aに以下のようなテストデータを投入し、うるう年が正しく判定されるかテストしたくなったとします。

テストデータ 期待値
2000 今年はうるう年です
2020 今年はうるう年です
2100 今年はうるう年ではありません

さて、どのようにテストしますか?

そうなんです。これではテストができません。
なぜなら、関数A・Bともに「現在の西暦を求める処理」と「うるう年を判断する仕組み」が紛れ込んでしまっており、それぞれの機能として独立していないからです。

したがって、このようにするのが良いでしょう。

def A(year):
    if isleap(year):
        print('今年はうるう年です')
    else:
        print('今年はうるう年ではありません')

このように、西暦情報は外から投入するようにします。
これで10年前であろうが100年前であろうが200年後であろうが、テストデータが複数あろうが関数は一つで済みます。

モジュール結合度には評価基準がある

モジュール間結合度には種類があります。

評価基準 モジュール間結合度 モジュール独立性
内部結合 高い 低い
共通結合
外部結合
制御結合
スタンプ結合
データ結合 低い 高い

では、ここからはコード付きでまとめていきます。
今回はPythonで再現してみます。

内部結合

これに関しては今どきほぼありえなくて、なかなか再現が難しいのですが...
強いて言うならこんな感じでしょうか。

こんなモジュールがあります。
username, level, attack, defence等がグローバル変数として定義されています。

moduleA.py
username = 'hogehogekun'
level = 25
attack = 20
defence = 5

def show_user_status():
    print('ユーザ名:' + username)
    print('レベル:' + str(level))
    print('攻撃力:' + str(attack))
    print('防御力:' + str(defence))

これを利用するコードがあったとします。

main.py
import moduleA

#レベルを1上げる
moduleA.level += 1

#moduleAの関数を利用してステータスを表示
moduleA.show_user_status()
結果
ユーザ名:hogehogekun
レベル:26
攻撃力:20
防御力:5

レベルは初期値が25でしたが、1増えています。
挙動としては問題ありませんが、moduleAの挙動はmainモジュールの挙動に深く依存してしまっています。

共通結合

では、ユーザの情報を一つのクラスにまとめ、それらを一つのリストで管理していたとしましょう。

moduleA.py
class User:
    def __init__(self, username, level, attack, defence):
        self.username = username
        self.level = level
        self.attack = attack
        self.defence = defence

    def show_user_status(self):
        print('ユーザ名:' + self.username)
        print('レベル:' + str(self.level))
        print('攻撃力:' + str(self.attack))
        print('防御力:' + str(self.defence))

#ユーザの一覧をリストで管理
user_list = [User('hogehogekun', 75, 90, 80), User('fugafugakun', 10, 5, 7)]

そして、このモジュールを利用するmainA内に2つの関数があったとします。

mainA.py
import moduleA

def funcA():
  del(moduleA.user_list[0])

def funcB():
  print(moduleA.user_list[0].username)


#funcA、funcBの順で実行
funcA()
funcB()
結果
fugafugakun

今回、あろうことかfuncAはグローバルなリストの先頭要素を削除してしまいました。
funcBが覗いたときには、hogehoge君はとっくに消えてしまい、fugafugaのみが残っていました。
ちなみにfuncBがmoduleA.user_list[1]を参照していた場合はIndexErrorが発生してしまいます。
このように、共通結合では共通のデータ構造の一部を変更したり削除したりした場合には、その共通部品を参照している全モジュールを見直す必要が出てきます。

外部結合

共通結合と非常に良く似ていますが、共有されている情報がリストやオブジェクトなどのデータ構造というよりは単体のデータの集まりという認識です。

今回は、仮に累積ユーザ数とサービスステータスという情報があったとします。

moduleA.py
class User:
    def __init__(self, username, level, attack, defence):
        self.username = username
        self.level = level
        self.attack = attack
        self.defence = defence

    def show_user_status(self):
        print('ユーザ名:' + self.username)
        print('レベル:' + str(self.level))
        print('攻撃力:' + str(self.attack))
        print('防御力:' + str(self.defence))

user_count = 123091 #累計ユーザ数
service_status = 200 #サービスの状況
main.py
import moduleA

def funcA():
    print(moduleA.user_count)

def funcB():
    print(moduleA.service_status)

funcA()
funcB()
結果
123091
200

このコードは挙動上の問題はありません。ユーザ数もサービスステータスもfuncA、funcB内で正しく取得できています。しかし、moduleA.pyのservice_statusが数値型ではなく、文字型になるなどの仕様変更があった場合には、該当する情報を参照しているfuncBの修正が必要になります。

制御結合

制御結合は、呼び出し先の関数の処理分けを引数を使って操作します。
今回のコードでは、some_commandに1を渡すか2を渡すかで異なる処理が行われます。

moduleA.py
class User:
    def __init__(self, username, level, attack, defence):
        self.username = username
        self.level = level
        self.attack = attack
        self.defence = defence

    def some_command(self, command_id):
        if command_id == 1: #ステータス表示コマンド
            print('ユーザ名:' + self.username)
            print('レベル:' + str(self.level))
            print('攻撃力:' + str(self.attack))
            print('防御力:' + str(self.defence))

        elif command_id == 2: #レベルアップコマンド
            print(self.username + 'のレベルが1上がった!')
            self.level += 1

main.py
from moduleA import User

user1 = User('hogehogekun', 40, 20, 20)
user1.some_command(1)
user1.some_command(2)
結果
ユーザ名:hogehogekun
レベル:40
攻撃力:20
防御力:20
hogehogekunのレベルが1上がった!

コマンドという情報を外部から渡しているため一見良さそうには見えます。
some_commandを呼び出す別のモジュールは、some_commandの内部構造を知っている必要があります。
したがって、結合度は比較的高めです。

スタンプ結合

いつもどおりのUserクラスです。
今回はmain.pyのfuncAとfuncBのやり取りに着目します。
funcA内でfuncBを呼び出し、引数としてリストを渡すというなんだかそれっぽいコードに見えます。

moduleA.py
class User:
    def __init__(self, username, level, attack, defence):
        self.username = username
        self.level = level
        self.attack = attack
        self.defence = defence

    def show_user_status(self):
        print('ユーザ名:' + self.username)
        print('レベル:' + str(self.level))
        print('攻撃力:' + str(self.attack))
        print('防御力:' + str(self.defence))

別モジュールではこのような動きをしていたとします。

main.py
from moduleA import User

def funcA():
    user_list = [User('hogehogekun', 20, 10, 10), User('fugafugakun', 99, 99, 99), User('piyopiyokun', 99, 99, 99)]
    funcB(user_list)

def funcB(user_list):
    print(user_list[2].username)

funcA()
結果
piyopiyokun

この時点では何も問題ありませんが、例えばfuncAの要素数が変わってしまった場合、funcBは影響を受けます。

main.py
def funcA():
    user_list = [User('hogehogekun', 20, 10, 10), User('fugafugakun', 99, 99, 99)]
    funcB(user_list)

def funcB(user_list):
    print(user_list[2].username)

funcA()
結果
IndexError: list index out of range

スタンプ結合ではリストごと受け渡しを行ってはいるものの、呼び出し側モジュールでは一部の情報しか利用していません。今回だとfuncBは3つの要素を持つリストを渡されているにも関わらず、3つめの要素しか利用していません。このとき、スタンプ結合ではfuncB側で利用していない要素(今回はその個数)に変更が入ったとしても、funcBは影響を受けてしまう可能性があります。

データ結合

複数のモジュール間で受け渡す情報が最小限になります。
スタンプ結合の例を踏襲すると、このような感じです。

moduleA.py
class User:
    def __init__(self, username, level, attack, defence):
        self.username = username
        self.level = level
        self.attack = attack
        self.defence = defence

    def show_user_status(self):
        print('ユーザ名:' + self.username)
        print('レベル:' + str(self.level))
        print('攻撃力:' + str(self.attack))
        print('防御力:' + str(self.defence))
main.py
def funcA():
    user_list = [User('hogehogekun', 20, 10, 10), User('fugafugakun', 99, 99, 99), User('piyopiyokun', 99, 99, 99)]
    funcB(user_list[2])

def funcB(target_user):
    print(target_user.username)

funcA()
結果
piyopiyokun

funcBではpiyopiyokunのみを処理の対象としているので、そもそも受け渡すデータはuser_list[2]のみとします。これでuser_listの個数が減った場合、funcAに影響はありますがfuncBに影響は無いはずです。
そもそもuser_listの個数が減るということはfuncAを修正しているということですから、影響範囲はかなり小さくなっているのではないでしょうか。

このように、必要な情報のみをやりとりすることによってモジュールの結合度を弱くすることができます。

追記:手元にあった本を読んでいたら、「呼び出される側のモジュールは受け取った引数によって呼び出した側のデータを直接操作できるようにする」と書いてありました。つまり参照渡しをせよということになるようです。この辺りは開発者や開発手法によって考え方にばらつきがあるようでした。

まとめ

モジュールの強度は高く、モジュール間の結合度は低く設定することで色々なメリットがあります。

  • テストがしやすい
  • 再利用性が高まる
  • 仕様変更時の影響範囲が狭くなる

等ですね!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした