⛓️

Swiftで結合度を理解する

結合度 についてSwiftでの具体例を交えて整理してみる。基本情報技術者資格などで名前は知っている人が多いが、実務で使える知識にしたい。

プログラミングにおいて、モジュール同士の密接さ、依存の度合いを表す尺度を 結合度(Coupling) という。 結合度が低いことを 疎結合(Loose Coupling) 、結合度が高いこと 密結合(Tight Coupling) という。

凝集度など別の尺度も考慮する必要があるため、単純に低ければ低いほど良い訳ではないが、一般的には疎結合であるとそのモジュールの保守性向上する。

結合度の種類

結合度には結合度の各段階に応じて以下の6種類名前が付けられている。これらは記載の順に疎結合になっていく。

  • 内容結合(Content Coupling)
  • 共通結合(Common Coupling)
  • 外部結合(External Coupling)
  • 制御結合(Control Coupling)
  • スタンプ結合(Stamp Coupling)
  • データ結合(Data Coupling)

さらに引数なしのメソッド呼び出し関係を差す、メッセージ結合(Message Coupling)、全く無関係な無結合(No Coupling)もある。

内容結合(Content Coupling)

  • 別名: 病理学的結合(Pathological Coupling)
  • あるモジュールが別のモジュールの 内容(内部のデータや振る舞い) を直接参照している
  • アセンブラ言語などに見られるが、Swiftなど情報隠蔽が可能な高水準言語では基本的に見られない
  • 例)アドレスやポインタを直接参照している
// あまり意味がないので実装例は割愛

共通結合(Common Coupling)

  • 別名: グローバル結合(Global Coupling)
  • 共通領域に定義したデータ を複数のモジュールで参照している
  • 例)static変数を共有している
  • 共通領域のデータ
struct Common {
    static var data: Int = 0
}

struct Foo {
    func doSomething() {
        print(Common.data)
        // Fooでの共通領域のデータ変更が、
        // Barなど他の箇所でも予期せぬ副作用を起こす
        Common.data += 1
    }
}

struct Bar {
    func doSomething() {
        print(Common.data)
    }
}

外部結合(External Coupling)

  • あるモジュールが別のモジュールの 外部宣言したデータ を参照している
  • 例)public変数を参照
struct Foo {
    func doSomething() {
        var bar = Bar()
        print(bar.data)
        bar.data += 1
    }
}

struct Bar {
    var data: Int = 0

    func doSomething() {
        print(data)
    }
}

制御結合(Control Coupling)

  • あるモジュールが別のモジュールを 制御するためのデータ を渡している
  • 制御データによって振る舞いがかわる場合、呼び出し側は利用するモジュールの内部ロジックを知る必要がある
  • 呼び出されるモジュールは 論理的凝集 となる
  • 例)制御フラグをBool値で渡す
struct Foo {
    func doSomething() {
        let bar = Bar()
        bar.doSomething(flag: true)
    }
}

struct Bar {
    func doSomething(flag: Bool) {
        if flag {
            print("Bar")
        }
    }
}

スタンプ結合(Stamp Coupling)

  • 別名: データ構造結合(Data-structured Coupling)
  • あるモジュールが別のモジュールに 構造のあるデータ (独自に定義したオブジェクトなど)を渡している
  • データ構造が大きい場合、不要なデータまで渡してしまうことも
struct Data {
    let id: String
    let text: String
    let number: Int
}

struct Foo {
    func doSomething() {
        let bar = Bar()
        let data = Data(id: "01", text: "Foo", number: 1)
        bar.doSomething(data: data)
    }
}

struct Bar {
    func doSomething(data: Data) {
        print("\(data.text) \(data.number)")
    }
}

データ結合(Data Coupling)

  • あるモジュールが別のモジュールに構造のないデータ(プリミティブ型など)を渡している
struct Foo {
    func doSomething() {
        let bar = Bar()
        bar.doSomething(text: "Foo", number: 1)
    }
}

struct Bar {
    func doSomething(text: String, number: Int) {
        print("\(text) \(number)")
    }
}

感想

データ結合は結合度が低いのは理解できるが、引数が多い場合にはアンチパターンになると思う。 利用クラスが各引数の意味を理解して正しく値をセットする、引数の有無、名称、型に変更があった場合、影響箇所が増えるため。

この場合の解決方法としてはスタンプ結合に結合度を上げ、引数となるオブジェクトを渡す。このオブジェクトはパラメーターオブジェクトとも呼ばれる。もしも不要なプロパティが多くなったり、不要な依存関係が生まれる場合は、新たに必要なデータをまとめたオブジェクトを作るのも一つの方法。

MARTIN FOWLERの 「新装版 リファクタリング―既存のコードを安全に改善する」 にも 「長すぎるパラメーターリスト(Long Parameter List) の項目でコードスメルの一種としてが説明されている。

参考