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

モジュール結合度について

More than 3 years have passed since last update.

はじめに

20日目は、(株)スマートテック・ベンチャーズの串田(@eKushida)が担当します。

プログラム設計の段階で、良い設計・悪い設計とはと議論になったときに、
チーム内で一つの統一された基準を持つのは大事ですね。

今回は、モジュール分割に着目して、お話したいと思います。

評価軸 説明 良い設計 悪い設計
モジュール結合度 2つのモジュール間の結合度の強さを示す 弱い 強い
モジュール強度 1つのモジュールの内部の命令がどれほど深く関わり合っているかを示す 強い 弱い

一般的には、モジュールを変更しても、他のモジュールに与える影響が少ないことが望まれ、
モジュール結合度が弱く、モジュール強度が強いものが良いとされています。

昔、情報処理試験を勉強したとき、
モジュール強度は、「内と共に外を制すデータ」と丸暗記しました。

今回はさらに絞って、「モジュール結合度」について、
具体例を利用してご説明します。

モジュール結合度

モジュール結合度が弱いほど、独立性が高くなります。

結合度が弱い順に並べると下記のようになります。

# モジュール結合度 説明
1 データ結合 引数で単純なデータを渡すパターン
2 スタンプ結合 引数で構造体などのオブジェクトを渡すパターン
3 制御結合 引数の種類によって、メソッドの内の処理が変わるパターン
4 外部結合 単一のグローバルデータを参照しているパターン
5 共通結合 複数のグローバルデータを参照しているパターン
6 内容結合 他のオブジェクトの内部を参照しているパターン

つまり、コーディングの効率が良いからという理由で、
グローバルデータを多用するのは良くないということです。
(変数のスコープは狭いほど、良いとされています。)

具体的なイメージがわきづらいため、Swiftで例示してみます。

1. データ結合

データ結合は、結合度が低く、テストもしやすい。
1つの入力に対し、1つの出力になる。

サンプル
    func dataJoinTypeMethod(name: String, age: Int) -> String{
        return "\(name)さんは、\(age)歳です。"
    }
使い方
    func testDataJoinTypeMethod() {
        let result = account.dataJoinTypeMethod(name: "山田", age: 30)
        XCTAssertEqual(result, "山田さんは、30歳です。")
    }

2. スタンプ結合

スタンプ結合は、構造体のデータ型に依存する。
構造体内のプロパティを変更、削除すると影響を受ける。
1つの入力に対し、1つの出力になる。

サンプル
    func stampJoinTypeMethod(account: Account) -> String{
        return "\(account.name)さんは、\(account.age)歳です。"
    }
使い方
    func testStampJoinTypeMethod() {
        let result = account.stampJoinTypeMethod(account: Account(name: "山田", gender: .man, age: 30, mail: ""))
        XCTAssertEqual(result, "山田さんは、30歳です。")
    }
関連クラス
import UIKit

enum GenderType: String {
    case man = "男性"
    case woman = "女性"
}

struct Account {
    var name = ""
    var gender: GenderType?
    var age = 0
    var mail = ""
}

3. 制御結合

制御結合は、引数のパターンに依存する。
1つの入力に対して、Nパターンの出力になる。
今回は、2パターンの出力になる。

サンプル
    func controlJoinTypeMethod(name: String, gender: GenderType, age: Int) -> String{

        switch gender {
        case .man:
            return "\(name)君は、\(age)歳です。"

        case .woman:
            return "\(name)さんは、\(age)歳です。"
        }
    }
使い方
    func testControlJoinTypeMethodForMan () {
        let result = account.controlJoinTypeMethod(name: "山田", gender: .man, age: 30)
        XCTAssertEqual(result, "山田君は、30歳です。")
    }

    func testControlJoinTypeMethodForWoman() {
        let result = account.controlJoinTypeMethod(name: "山田", gender: .woman, age: 30)
        XCTAssertEqual(result, "山田さんは、30歳です。")
    }

ちなみに、スタンプ結合と制御結合を組み合わせたパターンをハイブリッド結合と呼ぶ。
ただし、ハイブリッド結合は、モジュール結合度の分類によっては制御結合に分類されることもある。

サンプル
    func hybridJoinTypeMethod(account: Account) -> String{

        guard let gender = account.gender else {
            fatalError("genderは、nilです")
        }

        switch gender {
        case .man:
            return "\(account.name)君は、\(account.age)歳です。"

        case .woman:
            return "\(account.name)さんは、\(account.age)歳です。"
        }
    }
使い方
    func testHybridJoinTypeMethodForMan () {
        let result = account.controlJoinTypeMethod(account: Account(name: "山田", gender: .man, age: 30, mail: ""))
        XCTAssertEqual(result, "山田君は、30歳です。")
    }

    func testHybridJoinTypeMethodForWoman() {
        let result = account.controlJoinTypeMethod(account: Account(name: "山田", gender: .woman, age: 30, mail: ""))
        XCTAssertEqual(result, "山田さんは、30歳です。")
    }

4. 外部結合

外部結合は、単一のグローバルデータ(例 UserDefaults)に依存する。
1つの入力に対して、Nパターンの出力になる。
今回は、2パターンの出力になる。

例えば、他の処理からグローバルデータが変更されると
該当メソッドの出力に影響を与える。

サンプル
    func externalJoinTypeMethod(name: String) -> String{

        let userDefaults = UserDefaults.init()
        let acountType = userDefaults.integer(forKey: "acountType")

        switch acountType {
        case AccountType.premium.rawValue:
            return "\(name)さんは、プレミアム会員です。"

        case AccountType.general.rawValue:
            return "\(name)さんは、一般会員です。"

        default:
            fatalError("このパターンは例外です")
        }
    }
使い方
    func testExternalJoinTypeMethodForPremium() {
        userDefaults.set(AccountType.premium.rawValue, forKey: "acountType")

        let result = account.externalJoinTypeMethod(name: "山田")
        XCTAssertEqual(result, "山田さんは、プレミアム会員です。")
    }

    func testExternalJoinTypeMethodForGeneral() {
        userDefaults.set(AccountType.general.rawValue, forKey: "acountType")

        let result = account.externalJoinTypeMethod(name: "山田")
        XCTAssertEqual(result, "山田さんは、一般会員です。")
    }
関連クラス
enum AccountType: Int {
    case premium
    case general
}

グローバルデータの依存性を排除すると、モジュール結合度が弱くなる。

リファクタリングの例
    func controlJoinTypeMethod(name: String, acountType: AccountType) -> String {

        switch acountType {
        case .premium:
            return "\(name)さんは、プレミアム会員です。"

        case .general:
            return "\(name)さんは、一般会員です。"
        }
    }

5. 共通結合

共通結合は、複数のグローバルデータ(例 UserDefaults)に依存する。
外部結合とほぼ同じだが、複数データに依存するので、外部結合より結合度が高い。
1つの入力に対して、Nパターンの出力になる。
今回は、4パターンの出力になる。

サンプル
    func commonJoinTypeMethod(name: String) -> String{

        let userDefaults = UserDefaults.init()
        let acountType = userDefaults.integer(forKey: "acountType")
        let versionStr = userDefaults.string(forKey: "ver")

        switch acountType {
        case AccountType.premium.rawValue where versionStr == "iOS10":
            return "プレミアム会員の\(name)さんは、Apple Pay決済 or カード決済できます。"

        case AccountType.premium.rawValue where versionStr != "iOS10":
            return "プレミアム会員の\(name)さんは、カード決済できます。"

        case AccountType.general.rawValue:
            return "一般会員の\(name)さんは、現金のみで決済できます。"

        default:
            fatalError("このパターンは例外です")
        }
    }
使い方
    func testCommonJoinTypeMethodForPremiumAndIOS10() {
        userDefaults.set(AccountType.premium.rawValue, forKey: "acountType")
        userDefaults.set("iOS10", forKey: "ver")

        let result = account.commonJoinTypeMethod(name: "山田")
        XCTAssertEqual(result, "プレミアム会員の山田さんは、Apple Pay決済 or カード決済できます。")
    }

    func testCommonJoinTypeMethodForPremiumAndIOS9() {
        userDefaults.set(AccountType.premium.rawValue, forKey: "acountType")
        userDefaults.set("iOS9", forKey: "ver")

        let result = account.commonJoinTypeMethod(name: "山田")
        XCTAssertEqual(result, "プレミアム会員の山田さんは、カード決済できます。")
    }

    func testCommonJoinTypeMethodForGeneralAndIOS10() {
        userDefaults.set(AccountType.general.rawValue, forKey: "acountType")
        userDefaults.set("iOS10", forKey: "ver")

        let result = account.commonJoinTypeMethod(name: "山田")
        XCTAssertEqual(result, "一般会員の山田さんは、現金のみで決済できます。")
    }

    func testCommonJoinTypeMethodForGeneralAndIOS9() {
        userDefaults.set(AccountType.general.rawValue, forKey: "acountType")
        userDefaults.set("iOS9", forKey: "ver")

        let result = account.commonJoinTypeMethod(name: "山田")
        XCTAssertEqual(result, "一般会員の山田さんは、現金のみで決済できます。")
    }

共通結合も外部結合同様に、グローバルデータの依存性を排除すると、
モジュール結合度が弱くなる

6. 内容結合

内容結合は、関連メソッドの変更に依存する。
デバッグ時は、該当メソッドだけでなく、関連メソッドも確認する必要がある。

また、該当メソッドの変更が関連メソッドの変更に影響する場合がある。

サンプル
    func internalJoinTypeMethod(name: String) -> [Employee]{

        //関連メソッド
        let employees = unJoinTypeMethod()

        let result = employees.filter{
            $0.name.contains(name)
        }.
        return result
    }

    func unJoinTypeMethod() -> [Employee]{
        return [Employee(name: "山田"),Employee(name: "佐藤"),Employee(name:"ワタナベ")]
    }
使い方
    func testInternalJoinTypeMethod() {
        let results = account.internalJoinTypeMethod(name: "山田")

        results.forEach {
            XCTAssertEqual($0.name, "山田")
        }
    }

内部結合は、関連メソッドの依存性を排除すると、モジュール結合度が弱くなる。

リファクタリングの例
    func refactorInternalJoinTypeMethod(name: String) -> [Employee]{

        let employees = unJoinTypeMethod()
        return dataJoinTypeMethod(name: "山田", employees: employees)
    }

    func unJoinTypeMethod() -> [Employee]{
        return [Employee(name: "山田"),Employee(name: "佐藤"),Employee(name:"ワタナベ")]
    }

    func dataJoinTypeMethod(name: String, employees: [Employee]) -> [Employee]{

        let result = employees.filter{
            $0.name.contains(name)
        }
        return result
    }

まとめ

最後までご覧いただき、ありがとうございました。
場当たり的にメソッドを定義するのではなく、モジュール結合度や強度を考慮した上で、
どのパターンを選択するか決めて行きたいと思います。

誤り等ございましたら、ご指摘頂けますと、幸いです。

スマートテック・ベンチャーズでは、未経験だけどiOSの開発をやりたい!という人を募集しています。
Advent Calendarのスマートテック・ベンチャーズページに会社およびWantedlyのURLをのせていますので、興味のある方は是非ご覧ください。
https://www.wantedly.com/companies/st-ventures/info

明日は、@okuderapさんです。
お楽しみに!

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