プログラミング

結合の歴史雑まとめと『ソフトウェア設計の結合バランス』

ソフトウェア設計の結合バランス』はとても良い本だと思います。結合(coupling)を「避けるべきもの」ではなく「設計するもの」として捉え直した本だと私は読みました。個人的には、今後広く参照されるようになるんじゃないかなと感じています。

ただ、1960年代後半から積み重なってきた結合の議論というものは様々あり、現代までの歴史的な文脈やこれまでのデファクトスタンダードという観点において、記載はあるもののまだまだ深ぼるべき点はありそうだなと思い色々調査してみました。本記事では、結合という概念がどう生まれ、どう変遷してきたかを整理してみます。

そもそも「結合」とは何か

結合(coupling)とは、モジュールやコンポーネント同士の依存関係の強さを表す概念です。

直感的には、

  • 結合が強い:あるモジュールを変えると、別のモジュールも一緒に変える必要がある
  • 結合が弱い:片方を変えても、もう片方には影響しない

一方、一般的に対となる概念として凝集度(cohesion)があります。凝集度とは、モジュール内の要素がどれだけ関連性を持っているか、という概念です。

  • 凝集度が高い:モジュール内の要素が密接に関連し、1つの責務を持つ
  • 凝集度が低い:モジュール内の要素がバラバラで、無関係な責務が混在している

という感じです。

よく聞くスローガンは「疎結合・高凝集」ですね。

本記事のテーマである『ソフトウェア設計の結合バランス』は、この「疎結合一律で善」というデファクトの考え方を相対化するものだと思っています。それを理解するために、まずは「結合」がどう生まれ、どう変わってきたかを順番に見ていきます。

構造化設計の時代(1960年代後半〜1970年代)

結合と凝集度の概念を最初に体系化したのはLarry Constantineとされています[1]

広く知られるきっかけになったのは、Wayne Stevens, Glenford Myers, Larry Constantineの3名による論文「Structured Design」(IBM Systems Journal Vol. 13, No. 2, 1974)です。この論文では現代まで使われる結合のレベル分けと、構造化チャートを含む構造化設計の基本が提示されました。

結合度

「モジュール結合」は、強い順に概ね以下のレベル分けです[2]

  1. 内容結合(Content Coupling):

    • 複数のモジュール間で、内部のデータ構造や処理ロジックを直接共有する形
    • 例:モジュールAが、モジュールBの内部変数を直接書き換えたり、モジュールBの途中にGOTOで飛び込んだりする
  2. 共通結合(Common Coupling):

    • グローバル変数などを利用して複数のモジュールが同じデータを共有する形
    • 例:int global_count = 0; を複数モジュールから読み書きしている
  3. 制御結合(Control Coupling)

    • モジュール側の処理ロジックを、呼び出し側がフラグ引数などを渡して制御する形
    • 例:process(data, mode)mode が1ならvalidation、2ならsave、3ならdelete、みたいな感じ
  4. スタンプ結合(Stamp Coupling):

    • データ構造を共有する形。ただし、呼び出し側はそのデータ構造の一部しか使わない場合もある
    • 例:process(user)のような形。userid, name, email を持っているが、processid しか使わない
  5. データ結合(Data Coupling):

    • 必要なデータだけを引数として渡す形
    • 例:process(userId) のような形。必要なデータだけを渡している

50年経った今でも、現代のコードレビューで「グローバル変数やめよう」「不要なフラグ引数を増やすな」みたいな話は当たり前のように出てきますよね(いや、そもそもしないし、あんまりないか)。まぁ、不用意なグローバル変数や挙動を変えるフラグ引数のようなコードはあんまり書かないですよね。

古典的な指針は「データ結合に近づけろ」、つまり必要なデータ項目だけを引数として渡せ、というものでした。一方で現代の言語では、ユーザ情報を渡すのに userId だけでなく User オブジェクト全体を渡すような書き方が一般的で、これは古典的にはスタンプ結合に分類されます。
これだけ見ると「現代のコードは古典的にはスタンプ結合だらけ」とも読めます。User型自体が「必要なデータをまとめた構造」として設計されているなら、実態は「データ結合に近いスタンプ結合」と見ることもできます。古典的な分類はあくまで「依存の強さ」を粗く測る指標なので、現代のコードでは『どんな構造を渡すか』までは指針として明示しない、と思っておくのが現実的かなと思います。

凝集度

ここまで結合の話をしてきましたが、対となる凝集度(Cohesion)にも触れておきます。凝集度も同じくConstantineらが構造化設計の時代に提案した概念で、「モジュール内の要素がどれだけ密接に関連しているか」を表します。

凝集度にも、結合のレベル分けと同様に「良い順」の段階があります。最良は機能的凝集(Functional Cohesion、1つの明確な責務だけを持つ)で、最悪は偶発的凝集(Coincidental Cohesion、関連のない処理が偶然集まっている)です。例えば「ファイルからユーザを読み取って、税金を計算して、メールを送信する」みたいに、責務が混じった関数は凝集度が低い典型例ですね。

  • 凝集度が高い:関連の強い要素が、同じモジュール(近い距離)に置かれている
  • 凝集度が低い:本来近くにあるべきでない要素が同じモジュール(近い距離)に押し込まれているか、関連の強い要素が別モジュール(遠い距離)に散らばっている

結合度も、別の見方をすれば「距離」の話です。

  • 結合度が低い:関連の弱い要素が、別モジュール(遠い距離)に分かれている
  • 結合度が高い:本来離すべき要素が同じモジュール(近い距離)に混在しているか、別モジュールに置いてあるのに強く依存している

整理すると、

  • 凝集度:近くに置くべきものを近くに置けているか
  • 結合度:離すべきものを離せているか

どちらも「要素同士の距離をどう設計するか」という同じ問題を、別角度から測っている指標とも読めます。後述する『ソフトウェア設計の結合バランス』の「距離」という3次元の1つは、凝集度・結合度の議論の中で暗黙的に扱われていた距離の概念を独立した評価軸として取り出したもの、と読むこともできるかなと思っています(あくまで個人的な解釈です)。

情報隠蔽

同時期、David Parnasが1972年に「On the Criteria To Be Used in Decomposing Systems into Modules」を発表し、情報隠蔽(Information Hiding)という別の角度からモジュール分割の指針を示しました。

情報隠蔽とは「モジュールの内部で行っている決定を、外(他のモジュール)から見えなくする」という考え方です。ここでの「決定」とは、例えば「データをどう持つか」「どんなアルゴリズムを使うか」「外から見せる入出力フォーマットは何か」といった、設計時に複数の選択肢から選んだ結果のことを指します。これらを公開せず、APIだけ外に出しておけば、後から内部実装を入れ替えても利用側には影響しない、というイメージですね。現代で言えば「クラスのprivateフィールド」「インターフェース越しの依存」「ヘッダファイル経由のC言語のモジュール分離」なんかが情報隠蔽の具体例にあたります。

そしてParnasの主張で重要なのは、「特に変更されそうな決定を優先的に隠せ」と踏み込んだ点です。論文の中で次のように述べています。

We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.

難しい設計上の決定や、変更されそうな決定のリストから始めることを提案する。その上で各モジュールはそのような決定を他のモジュールから隠すように設計する。

「変更されそうな決定を隠せ」と書いているのは、後述する『ソフトウェア設計の結合バランス』の「変動性」と完全に同じ概念とまでは言えませんが、地続きの発想だと感じます。構造化設計のモジュール結合の分類ではこの軸は明示されておらず、本書は3つの次元の1つとして拾い直しています。

関心の分離

少し脇道ですが、同じ1970年代にEdsger W. Dijkstra、あのダイクストラが「Separation of Concerns(関心の分離)」という言葉を使っています。代表的な出典は1974年のエッセイ「On the role of scientific thought」(EWD447)で、Dijkstra自身もここで「what I sometimes have called “the separation of concerns”」と書いており、それ以前から使っていた言葉ぶりです。

これ自体は「モジュールはこう分割せよ」という設計指針ではなく、もっと上位の思考論です。計算機科学も科学的思考で考えるべきだ、みたいな感じで、正直ちょっと私自身はちゃんと理解できたわけではないです。ただし「分けるなら関心ごとに分けろ」という現代の設計原則に大きな影響を与えた考え方の1つではあり、レイヤードアーキテクチャやMVC、各種クリーンアーキテクチャで耳にする「関心の分離」も、この流れの上にあると言えます。

こう見ると、結合・凝集、情報隠蔽、関心の分離と、現代の設計原則の土台はおおむね1970年代に出揃っていたわけです。

オブジェクト指向の時代(1980年代後半〜1990年代)

1980年代後半から1990年代にかけて、オブジェクト指向プログラミング(OOP)が広がります。

OOPでは「モジュール」が「クラス」に置き換わり、結合の話もクラス同士、オブジェクト同士の話に再解釈されていきます。Grady Boochは『Object-Oriented Analysis and Design with Applications』(3rd Edition, 2007)の中で、モジュール性を次のように定義しています。

Modularity is the property of a system that has been decomposed into a set of cohesive and loosely coupled modules.

モジュール性とは、システムが凝集性の高い疎結合なモジュール群へ分解されているという性質である。

基本的に構造化設計の結合・凝集のスローガンを継承する形ですね。なお、初版は1991年、2版は1993年に出ているので、OOPの時代の言説としては概ね地続きと言ってよいと思います。

ただ、今までの話と異なる点で、OOPの世界では「継承」や「多態性」、「インターフェース」というものが出てきます。継承により、サブクラスがスーパークラスの内部実装に依存してしまう問題が発生したり、一方で依存性逆転の法則が重要であるといったようなOOPならではの結合度の話があります。構造化設計時代の「データ結合vs共通結合」だけでは語りきれない結合の形が出てきます。

カプセル化

OOPで広まったもう1つの概念がカプセル化(Encapsulation)です。クラスにデータと操作(メソッド)をまとめ、外からは公開APIだけを触らせる仕組みのことですね。

コナーセンス

1992年にMeilir Page-Jonesが「Connascence(コナーセンス)」という新しい概念を提案します[3]

コナーセンスの定義は「2つの要素A, Bがあって、Aの変更がBの変更(または少なくとも確認)を要求する関係」です。構造化設計の「モジュール結合の段階」と似ているようで、扱う粒度が一段細かく、分類軸も違います。

コナーセンスの分類と強さ

コナーセンスは「2つの要素の間の依存」を細かく分類して扱います。Page-Jonesは静的(ソースコードを読めば判別できるもの)と動的(実行時に発生するもの)の2グループに分け、強さの順序つきで9種類ほどに整理しています

  • 名前のコナーセンス:同じ名前を共有することによる結合(メソッド名のリネームが両方に影響する)
  • 型のコナーセンス:同じ型を共有することによる結合
  • 意味のコナーセンス:同じ意味の解釈(マジックナンバーなど)を共有することによる結合
  • 位置のコナーセンス:引数の位置に依存することによる結合(順序を間違えるとバグになる)
  • アルゴリズムのコナーセンス:同じアルゴリズム前提への依存(例:ハッシュ値の生成方式が両側で一致している必要がある)
  • 実行順のコナーセンス:「Aを呼んでからBを呼ばないと壊れる」のような時間順序依存
  • タイミングのコナーセンス:並行処理での順序やタイミング依存
  • 値のコナーセンス:同じ値を複数箇所が持つ依存(DBに保存された値と定数が一致していないと壊れる、など)
  • 同一性のコナーセンス:同じインスタンスを参照していなければならない依存

コナーセンスの「強さ」(つまり、どれくらい厄介か)は、次の3軸で評価するとされています。

  1. Degree(度合い):関係する要素の数。10個のクラスが同じ意味に依存しているより、2クラスだけのほうがマシ
  2. Locality(局所性):同じスコープ内(同じ関数内、同じクラス内)なら影響が局所的だが、別パッケージ・別サービスをまたぐと深刻
  3. 静的か動的か:動的のほうが一般的に発見・修正コストが高い

「ソフトウェア設計の結合バランス」でも言及されていますが、この「動的」のほうは既存の結合度の分類ではうまく表現できない軸でもあります。

また、コナーセンスと古典的な結合度では評価の向きが食い違うケースもあります。例えば「位置のコナーセンス」(引数の位置に依存する結合)は、同じ関数内で完結する呼び出しならほぼ無害ですが、公開APIやRPC越しの引数になると一気に重い結合になります。逆に、構造化設計の「データ結合」は古典的には弱い結合とされていますが、サービスをまたいで頻繁に変わるデータを渡している場合は、実質的にはかなり強い結合になっています。同じ依存関係でも、距離や変動性を加味すると評価が変わる、というのが後の3次元モデルにつながる視点です。

ちなみに:リファクタリングへの使い方

3軸を踏まえると、リファクタリングの指針が自然に出てきます。

  • できれば動的コナーセンスを静的コナーセンスに変える(コンパイル時に気づける形に持っていく)
  • 同じコナーセンスでも、スコープを狭くする(クラス内・関数内に閉じる)
  • 強い静的コナーセンスは、より弱い静的コナーセンスに置き換える

例えば、引数の順序に依存する「位置のコナーセンス」を、TypeScriptで {from, to} のようなオブジェクト引数にすると「名前のコナーセンス」に変わります。前者で起きていた「引数を逆に渡すバグ」は、後者ではキー名のtypoという別の形に変換され、コンパイラに見つけてもらえる確率が上がります。これが「位置 → 名前」と1段弱い側へ置き換えるリファクタリングの一例です。

アーキテクチャレベルの結合(2000年代〜2010年代)

2000年代に入ると、結合の議論はクラスやモジュールという単位から、サービスやコンテキストの境界という大きな単位に広がっていきます。

DDDとBounded Context

Eric Evansは『Domain-Driven Design:Tackling Complexity in the Heart of Software』(2003) で「Bounded Context(境界づけられたコンテキスト)」を提唱しました。私も完璧に理解しているわけではないですが、簡単に言えば「同じ用語でも、コンテキストが違えば意味が違う」「だからモデルとコードはコンテキスト単位で分割しろ」という考え方です。

マイクロサービスと「分散モノリス」

2010年代に入ると、マイクロサービスアーキテクチャが流行ります。

一方で同じ時期に、「分散モノリス(Distributed Monolith)」と呼ばれる現象、アンチパターンが顕在化します。サービスは分割されているけれども、デプロイの単位として独立しておらず、結局1つのモジュールを変えると全サービスを直さないといけない、というやつです。

この問題は、構造化設計時代の「モジュール結合の段階」だけでは綺麗に説明しきれません。「データ結合」レベルの粒度であっても、サービスをまたいで頻繁に変わるなら結合は実質的に強い、ということが起きるからです。『ソフトウェア設計の結合バランス』でみるなら、距離や変動性という軸を加味することで改めて捉えることが可能になるでしょう。

『ソフトウェア設計の結合バランス』(原著2024年/邦訳2025年)

ようやく本題です。『ソフトウェア設計の結合バランス』(原著『Balancing Coupling in Software Design』はAddison-Wesleyから2024年、日本語版はインプレスから2025年10月に発売)は、上記50年分の議論を「3つの次元」で整理し直した本だと思っています。

結合の3つの次元

本書のフレームワークは、結合を次の3つの軸で評価します。

  1. 強度(Strength / Integration Strength):モジュール間で共有される知識の量
  2. 距離(Distance):モジュール同士の物理的・論理的な離れ具合
  3. 変動性(Volatility):モジュールがどれくらいの頻度で変わるか

「強度」は構造化設計の「モジュール結合」やコナーセンスを取り込み直したものです。本書では「侵入結合・機能結合・モデル結合・コントラクト結合」の4段階に整理されています。

「距離」は、これまでの結合議論ではあまり明示的には扱われてこなかった軸です。本書では物理的・論理的に「結合した2つの要素がどれだけ離れているか」を見ます。

  1. 同じ関数内 / 同じファイル内
  2. 同じクラス内
  3. 同じパッケージ・モジュール内
  4. 同じプロセス内(別ライブラリ)
  5. 同じネットワーク内の別プロセス(別サービス)
  6. 別ネットワーク・別組織のサービス

というイメージで、下に行くほど距離が遠くなります。距離が遠くなるほど、結合があった時の変更コストは跳ね上がります。同じ関数内の2つの処理を直すのはファイル1つ開けば済みますが、別サービスにまたがる処理を直すには両サービスのコードを編集し、それぞれデプロイし、デプロイ順序や下位互換性まで考える必要があります。マイクロサービスの「分散モノリス」が辛いのも、距離だけが遠くなって調整コストが跳ね上がった状態だからです。

「変動性」は、その名のとおり、どれくらいの頻度で変わるかという話です。前述したParnasの「変更されそうな決定を隠せ」と地続きの軸ですね。

詳細な話は本を改めて読むほうが良いでしょう。

なぜ「バランス」なのか

本書は組み合わせを使った「均衡(バランス)」で結合を評価します。説明のために各次元を「高い/低い」の2値に単純化すると、組み合わせは2³=8通りです(本書では実際にはより細かいモデルが提示されますが、ここでは粗い理解として2値で考えます)。

例えば次のようになります。

  • 強度:高 / 距離:近 / 変動性:高 → バランスがある程度取れている
    • 結合度の高い頻繁に変更のある関数2つを同じファイルに置く、というケース。近くにあるので変更を一緒に追えるため、強い結合と高い変動性も受け止められる。ただ、チームによってはこれを許容するかどうかは分かれるかも。
  • 強度:低 / 距離:遠 / 変動性:低 → バランスが取れている
    • 安定したサードパーティライブラリにAPI経由で依存しているような状態。距離が遠くても、強度が低く滅多に変わらないので困らない
  • 強度:高 / 距離:遠 / 変動性:高 → バランスが悪い
    • 頻繁に一緒に変わる責務が、別のマイクロサービスに分かれているような状態。いわゆる分散モノリス。リリースの度に複数サービスを同時に直すことになり、距離の遠さがコストとして効いてくる

「強い結合は常に悪」ではない、という点が個人的には大事だと感じました。3つの次元の組み合わせで判断します。例えば「強度が高」くても「距離が近く」「変動性が低い」状況なら問題になりにくい、という結論が出てきます。逆に言えば、「疎結合・高凝集」を一律に追いかけるのはやや雑なルールだとも読めます。変動性と距離を見て「結合させていい場所」と「離すべき場所」を分けて考えるほうが現実的、というのが本書の主張だと理解しています。

本記事では割愛しますが、本書では安定性・実コスト・メンテナンスの労力・均衡性など、複数の観点でのバランスの取り方が具体的に書かれています。詳細は本書を参照してください。

まとめ

結合という概念の歴史は一定長く、そして何よりも昔の知識がまだ生きて使われているのがすごい。ソフトウェア設計の結合バランスは、この歴史を一定まとめ、1つの地図に起こしたもののように感じます。いい本。


  1. Computer Pioneers - Larry L. Constantine。1968年のNational Symposium on Modular Programmingでの発表が初出とされます。 ↩︎

  2. 文献や解説によっては「外部結合(External Coupling)」を含めて6段階で紹介されることもあります。ここでは代表的な5段階を扱います。 ↩︎

  3. Meilir Page-Jones, “Comparing Techniques by Means of Encapsulation and Connascence”, 1992。後に『What Every Programmer Should Know About Object-Oriented Design』(1995)や『Fundamentals of Object-Oriented Design in UML』(1999)で詳述されました。 ↩︎