😺

モジュールの強度についてまとめてみました

2024/08/30に公開

モジュールの強度(Cohesion)は、ソフトウェア設計において、モジュール内の要素(関数、データなど)がどれだけ強く関連しているかを示す指標です。モジュールの強度が高い(高凝集)場合、モジュール内の要素が同じ目的や機能に向かって密接に連携していることを意味します。一方、強度が低い(低凝集)場合、モジュール内の要素が関連性の低い異なる機能を持っていることを示します。

モジュールの強度は、ソフトウェアの保守性、再利用性、および理解のしやすさに大きく影響します。強度が高いモジュールは、変更の影響範囲が小さく、再利用しやすいという利点があります。

モジュールの強度の種類

モジュールの強度は、一般に次のような7つのレベルで分類されます。これらのレベルは、低い強度(望ましくない)から高い強度(望ましい)までの順に並んでいます。

  1. 偶然的強度 (Coincidental Cohesion)
  2. 論理的強度 (Logical Cohesion)
  3. 時間的強度 (Temporal Cohesion)
  4. 手順的強度 (Procedural Cohesion)
  5. 通信的強度 (Communicational Cohesion)
  6. 逐次的強度 (Sequential Cohesion)
  7. 機能的強度 (Functional Cohesion)

1. 偶然的強度 (Coincidental Cohesion)

偶然的強度とは、モジュール内の要素が特に関連性がないが、一緒にグループ化されている状態を指します。最も低い強度であり、望ましくない設計です。

: あるモジュールに、ユーザー入力をチェックする関数、ファイルを読み込む関数、およびデータを印刷する関数が含まれている場合。これらの関数は共通の目的を持たないため、偶然的に結合されたといえます。

# 低い強度(偶然的強度)の例
class UtilityModule:
    def read_file(self, filename):
        with open(filename, 'r') as file:
            return file.read()

    def send_email(self, recipient, message):
        print(f"Sending email to {recipient}: {message}")

    def calculate_sum(self, a, b):
        return a + b

# 関数は互いに関連性がない
utility = UtilityModule()
print(utility.read_file('data.txt'))
utility.send_email('example@example.com', 'Hello!')
print(utility.calculate_sum(5, 10))

2. 論理的強度 (Logical Cohesion)

論理的強度とは、モジュール内の要素が同じカテゴリに属するが、異なるタスクを実行する場合を指します。要素間の関連性が薄いため、あまり望ましくありません。

: さまざまな種類のデータの処理(例:文字列処理、数値計算、日付処理)を1つのモジュールにまとめる場合。これらは論理的には関連していますが、実際には異なるタスクを実行しています。

# 論理的強度の例
class DataProcessor:
    def process_text(self, text):
        return text.lower()

    def process_number(self, number):
        return number * 2

    def process_date(self, date_str):
        from datetime import datetime
        return datetime.strptime(date_str, '%Y-%m-%d')

# すべて「処理」に関係するが、異なるデータ型を扱っている
processor = DataProcessor()
print(processor.process_text('HELLO'))
print(processor.process_number(10))
print(processor.process_date('2024-08-30'))

3. 時間的強度 (Temporal Cohesion)

時間的強度とは、モジュール内の要素が特定のタイミングで一緒に実行されるが、それ以外の関連性がない場合を指します。初期化処理やクリーンアップ処理など、特定の時間帯で必要とされるタスクがまとめられることが多いです。

: プログラムの起動時に実行される初期化処理(例:ログの設定、データベース接続の確立、設定ファイルの読み込み)が1つのモジュールにまとめられている場合。

# 時間的強度の例
class InitializationModule:
    def setup_logging(self):
        print("Logging has been set up.")

    def connect_to_database(self):
        print("Connected to the database.")

    def load_configuration(self):
        print("Configuration has been loaded.")

# 初期化時に必要な設定をすべて行う
init_module = InitializationModule()
init_module.setup_logging()
init_module.connect_to_database()
init_module.load_configuration()

4. 手順的強度 (Procedural Cohesion)

手順的強度とは、モジュール内の要素が一連の手順として実行される場合を指します。要素間には手順的な関連性がありますが、すべての要素が同じデータに対して操作するわけではありません。

: ユーザーの入力を検証し、その後にデータベースに保存するモジュール。これらの操作は順序を守る必要がありますが、共通のデータを操作しているわけではありません。

# 手順的強度の例
class UserRegistration:
    def collect_user_data(self):
        print("Collecting user data.")

    def validate_user_data(self):
        print("Validating user data.")

    def save_user_data(self):
        print("User data has been saved.")

# 登録の手順に従って各メソッドを実行
registration = UserRegistration()
registration.collect_user_data()
registration.validate_user_data()
registration.save_user_data()

5. 通信的強度 (Communicational Cohesion)

通信的強度とは、モジュール内の要素が同じデータに対して操作を行う場合を指します。要素間でデータが共有されるため、関連性が強いです。

: データベースからデータを取得し、そのデータをフォーマットして表示するモジュール。これらの操作はすべて同じデータに対して行われます。

# 通信的強度の例
class DataHandler:
    def read_data(self):
        data = "Sample data"
        print(f"Reading data: {data}")
        return data

    def process_data(self, data):
        processed_data = data.upper()
        print(f"Processed data: {processed_data}")
        return processed_data

    def write_data(self, data):
        print(f"Writing data: {data}")

# すべてのメソッドが同じデータを扱っている
handler = DataHandler()
data = handler.read_data()
processed_data = handler.process_data(data)
handler.write_data(processed_data)

6. 逐次的強度 (Sequential Cohesion)

逐次的強度とは、モジュール内の要素が順番に実行され、一つの要素の出力が次の要素の入力として使用される場合を指します。要素間の関連性が非常に強いです。

: データを読み込み、そのデータを処理し、結果をファイルに保存するモジュール。これらの操作は、順次的に行われ、各ステップの出力が次のステップの入力になります。

# 逐次的強度の例
class DataProcessingPipeline:
    def extract_data(self):
        data = "raw data"
        print(f"Extracting data: {data}")
        return data

    def transform_data(self, raw_data):
        transformed_data = raw_data + " transformed"
        print(f"Transforming data: {transformed_data}")
        return transformed_data

    def load_data(self, transformed_data):
        print(f"Loading data: {transformed_data}")

# 一連の処理で各ステップの出力が次のステップの入力となる
pipeline = DataProcessingPipeline()
raw_data = pipeline.extract_data()
transformed_data = pipeline.transform_data(raw_data)
pipeline.load_data(transformed_data)

7. 機能的強度 (Functional Cohesion)

機能的強度とは、モジュール内のすべての要素が単一の明確なタスクや機能を実行する場合を指します。最も高い強度であり、最も望ましい設計です。すべての要素が一緒に働いて、モジュールの意図する機能を達成します。

: パスワードの暗号化を行うモジュール。すべての関数が暗号化のプロセスに関与し、単一の目的(パスワードの安全な暗号化)を達成します。

# 機能的強度の例
class PasswordManager:
    def hash_password(self, password):
        import hashlib
        hashed = hashlib.sha256(password.encode()).hexdigest()
        print(f"Password hashed: {hashed}")
        return hashed

    def verify_password(self, password, hashed_password):
        return self.hash_password(password) == hashed_password

# パスワードの管理に関連するすべての機能が含まれている
manager = PasswordManager()
hashed = manager.hash_password('my_secure_password')
is_valid = manager.verify_password('my_secure_password', hashed)
print(f"Password is valid: {is_valid}")

モジュールの強度の重要性

  • 高い強度(高凝集): ソフトウェアの保守性が高く、モジュールが単一の責任を持つため、理解しやすく、テストもしやすくなります。再利用性も高まります。
  • 低い強度(低凝集): ソフトウェアの保守性が低く、変更の影響範囲が広がりやすくなります。モジュールが複数の目的を持つため、理解しにくく、バグも発生しやすくなります。

モジュールの設計において、強度を高めることは、ソフトウェアの品質向上に繋がります。高凝集で低結合なモジュールを設計することが、優れたソフトウェア設計の基本です。

  • 偶然的強度論理的強度は避けるべきであり、モジュールの内部要素が強く関連するように設計することが重要です。
  • 機能的強度は最も望ましい設計であり、モジュールが単一の明確な機能を実行するように設計することで、保守性や再利用性が向上します。

これらの例を通じて、各強度の違いと、それがソフトウェア設計に与える影響を理解するのに役立ててください。

コードの結合度を上げる方法は?

コードの結合度を上げる(結合度を強くする)ことは、通常は望ましくない設計とされています。結合度が高いということは、モジュールやクラス、関数が他のモジュール、クラス、関数に強く依存していることを意味し、変更に対して脆弱で、再利用性やテストのしやすさが低下する原因となります。通常、ソフトウェア設計では、モジュールの結合度を低く保つ(疎結合にする)ことが推奨されます。

しかし、学習のために、結合度を高める設計方法を理解することは有益です。結合度を高める例を以下に示しますが、これらの手法は、特定の目的や制約がない限り、避けるべきとされる設計です。

1. 直接アクセスによる結合度の上昇

モジュールが他のモジュールの内部データやメソッドに直接アクセスすると、結合度が高くなります。

# ModuleA.py
class ModuleA:
    def __init__(self):
        self.data = "ModuleA data"

# ModuleB.py
from ModuleA import ModuleA

class ModuleB:
    def __init__(self):
        self.module_a = ModuleA()

    def access_module_a_data(self):
        # ModuleAの内部データに直接アクセス
        print(f"Accessing data from ModuleA: {self.module_a.data}")

# 使用例
module_b = ModuleB()
module_b.access_module_a_data()

解説: ModuleBModuleAの内部データdataに直接アクセスしています。これにより、ModuleAの内部実装に依存し、結合度が高くなります。この設計は、ModuleAの内部が変更されるとModuleBにも影響を与えるため、保守性が低くなります。

2. グローバル変数の使用

複数のモジュールがグローバル変数を共有することで、結合度が上がります。

# global_config.py
GLOBAL_CONFIG = {
    "database_url": "localhost:5432",
    "api_key": "12345"
}

# ModuleA.py
from global_config import GLOBAL_CONFIG

def connect_to_database():
    db_url = GLOBAL_CONFIG["database_url"]
    print(f"Connecting to database at {db_url}")

# ModuleB.py
from global_config import GLOBAL_CONFIG

def fetch_api_data():
    api_key = GLOBAL_CONFIG["api_key"]
    print(f"Fetching data with API key: {api_key}")

# 使用例
connect_to_database()
fetch_api_data()

解説: GLOBAL_CONFIGというグローバル変数を複数のモジュールで共有しています。これにより、どのモジュールでもGLOBAL_CONFIGを変更できるため、結合度が高くなります。この設計は、GLOBAL_CONFIGが変更された場合、すべての依存するモジュールに影響を与えるため、保守性が低くなります。

3. ハードコーディングされた依存関係

モジュールやクラスが他のモジュールやクラスを直接参照し、その実装に依存する場合、結合度が高くなります。

# database.py
class Database:
    def connect(self):
        print("Connecting to database...")

# service.py
from database import Database

class Service:
    def __init__(self):
        self.db = Database()  # Databaseクラスにハードコードされた依存

    def perform_action(self):
        self.db.connect()
        print("Performing action using the database.")

# 使用例
service = Service()
service.perform_action()

解説: ServiceクラスはDatabaseクラスを直接参照し、インスタンス化しています。これは、Databaseクラスが変更されるとServiceクラスも変更が必要になる可能性があるため、結合度が高い設計です。

4. 複数の関数で同じロジックを繰り返す

関数やメソッドが同じロジックを繰り返している場合、変更が必要なときに複数の場所を修正する必要があるため、実質的に結合度が高くなります。

def calculate_discount(price):
    if price > 100:
        return price * 0.9  # 同じ割引計算ロジック
    return price

def calculate_final_price(price, tax):
    if price > 100:
        price = price * 0.9  # 同じ割引計算ロジック
    return price + tax

# 使用例
print(calculate_discount(150))
print(calculate_final_price(150, 10))

解説: 割引の計算ロジックが複数の関数に重複しています。変更が必要になった場合、すべての関数を変更する必要があるため、結合度が高くなります。このような設計は、バグの原因や保守性の低下を招きます。

まとめ

  • 結合度を上げる方法: 直接的な参照、グローバル変数の使用、ハードコーディングされた依存関係、重複したロジック。
  • 結合度が高いとどうなるか: モジュール間の依存が強く、変更に対して脆弱で、保守性や再利用性が低下する。

通常、結合度を上げることは避けるべきですが、特定の学習や設計の理由で結合度が高くなる場合もあります。良いソフトウェア設計では、モジュール間の結合度を低くし、モジュール内の凝集度を高く保つことが推奨されます。これは、ソフトウェアの保守性、再利用性、テストの容易さを向上させるためです。

Discussion