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さんです。

お楽しみに!