はじめに
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さんです。
お楽しみに!