プログラミング
Swift
プログラミング教育
More than 1 year has 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さんです。
お楽しみに!