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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```swift:サンプル
    func stampJoinTypeMethod(account: Account) -> String{
        return "\(account.name)さんは、\(account.age)歳です。"
    }
```
```swift:使い方
    func testStampJoinTypeMethod() {
        let result = account.stampJoinTypeMethod(account: Account(name: "山田", gender: .man, age: 30, mail: ""))
        XCTAssertEqual(result, "山田さんは、30歳です。")
    }
```
```swift:関連クラス
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パターンの出力になる。

```swift:サンプル
    func controlJoinTypeMethod(name: String, gender: GenderType, age: Int) -> String{
                
        switch gender {
        case .man:
            return "\(name)君は、\(age)歳です。"
            
        case .woman:
            return "\(name)さんは、\(age)歳です。"
        }
    }
```
```swift:使い方
    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歳です。")
    }
```

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

```swift:サンプル
    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)歳です。"
        }
    }
```
```swift:使い方
    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パターンの出力になる。

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

```swift:サンプル
    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("このパターンは例外です")
        }
    }
```
```swift:使い方
    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, "山田さんは、一般会員です。")
    }
```

```swift:関連クラス
enum AccountType: Int {
    case premium
    case general
}
```

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

```swift:リファクタリングの例
    func controlJoinTypeMethod(name: String, acountType: AccountType) -> String {
        
        switch acountType {
        case .premium:
            return "\(name)さんは、プレミアム会員です。"

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

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

```swift:サンプル
    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("このパターンは例外です")
        }
    }
```
```swift:使い方
    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. 内容結合
内容結合は、関連メソッドの変更に依存する。
デバッグ時は、該当メソッドだけでなく、関連メソッドも確認する必要がある。

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

```swift:サンプル
    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:"ワタナベ")]
    }
```
```swift:使い方
    func testInternalJoinTypeMethod() {
        let results = account.internalJoinTypeMethod(name: "山田")
        
        results.forEach {
            XCTAssertEqual($0.name, "山田")
        }
    }
```

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

```swift:リファクタリングの例
    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さんです。
お楽しみに！