開発プロセス●●○○○

結合度と凝集度

Coupling and Cohesion ・ けつごうどとぎょうしゅうど

モジュール間の依存の強さと、モジュール内部のまとまりの良さ。設計品質を測る古典的な物差し。

✦ 30秒で試す — 航海文言を1行直しただけで、決済まで再テストになる8つの関数を1つずつ別の箱に分けると、結合度は?予想してみる

概要

結合度(coupling)は「モジュール同士がどれだけ強く依存し合っているか」、凝集度(cohesion)は「1つのモジュールの中身がどれだけ1つの目的にまとまっているか」を表す言葉です。ここでいうモジュールとは、関数・クラス・パッケージ・サービスなど、プログラムを分割する単位すべてを指します。良い設計の目標は昔から一貫していて、「疎結合・高凝集」— つまりモジュール間のつながりは細く、モジュール内部のまとまりは濃く、です。

この2つは半世紀にわたって使われ続けている設計品質の物差しで、コードレビューで「ここ密結合になってない?」と言うときも、マイクロサービスの分割単位を議論するときも、同じ概念が土台になっています。特定の言語やパラダイムに縛られない、ソフトウェア設計のもっとも基礎的な語彙のひとつです。

大事なのは、これが「変更のしやすさ」を測る物差しだという点です。結合が密なら、1か所を直したときの影響が他のモジュールへ波及します。凝集が低ければ、1つの修正のためにモジュール内の無関係なコードまで読み解く必要があります。どちらも、ソフトウェアを育て続けるコストに直結します。

なぜ生まれたか

この概念が言語化されたのは1970年代、Larry Constantine と Edward Yourdon らによる「構造化設計(Structured Design)」の中でです。当時、プログラムはどんどん大規模化していましたが、分割の仕方は各人の勘に任されていました。goto と共有データが絡み合った、いわゆるスパゲッティコードでは、1行の修正がどこに波及するか誰にも予測できず、修正のたびに別の場所が壊れる — 保守コストが開発コストを圧倒し始めていた時代です。

構造化設計は「プログラムをどうモジュールに分割すれば保守しやすいか」に初めて体系的な答えを与えようとし、その評価軸として結合度と凝集度を定義しました。「分割すればよい」のではなく「分割の良し悪しを測れる」ようにしたことが発明です。以来、オブジェクト指向、マイクロサービス、サーバレスとアーキテクチャの流行が移り変わっても、この物差し自体は一度も古びていません。分割の単位が関数からクラスへ、クラスからサービスへとスケールしただけです。

詳細

結合度 — 依存の「強さ」に段階がある

結合度は単なる「依存の有無」ではなく、依存の質に段階があります。構造化設計(と、それを受け継いだ Glenford Myers の整理)では、強い順におおよそ次の6段階が定義されました。

  • 内容結合: 相手の内部実装(内部の変数や、途中の行への飛び込み)を直接いじる
  • 共通結合: グローバル変数などの共有データ経由でつながる
  • 外部結合: 外から押し付けられたファイル形式・通信手順・装置のインターフェースを、複数のモジュールが共有する
  • 制御結合: フラグを渡して、相手の中の処理の分岐を外から操る(print(doc, isDraft) の isDraft のような引数)
  • スタンプ結合: 必要なのは一部なのに、構造体やオブジェクトを丸ごと渡す(住所だけ要るのに「顧客」全体を渡す)
  • データ結合: 必要なデータだけを、引数として明示的に渡す

実務でいちばん見落とされやすいのはスタンプ結合です。「顧客」オブジェクトを丸ごと受け取る関数は、使っていないフィールドの型が変わっただけでも巻き込まれ、テストでも無関係な項目まで埋めた顧客を用意しなければなりません。現代のコードに翻訳すると、「相手クラスの private な事情を知っている」「同じ可変状態を共有している」ほど密で、「明示的なインターフェースと必要最小限のデータだけでやり取りする」ほど疎、ということです。

依存はゼロにはできません。まったく依存し合わないモジュールは、そもそも協調して仕事ができないからです。目指すのは依存の削減ではなく、依存を「細く・明示的に・安定した境界に向ける」ことです。

凝集度 — まとまりの「純度」にも段階がある

凝集度にも段階があります。低い順に、偶発的 → 論理的 → 時間的 → 手続き的 → 通信的 → 逐次的 → 機能的、の7段階です。もっとも低いのは、たまたま同じ場所に置かれただけの「偶発的凝集」— 何でも入りの Utils クラスが典型です。「入出力に関係するもの」のように種類が似ているだけで束ねた「論理的凝集」、「起動時に実行するもの」のように同じタイミングで動くだけの「時間的凝集」、決まった手順の順に並べた「手続き的凝集」と続き、「同じデータを扱う処理」を集めた「通信的凝集」、前の処理の出力が次の入力になる流れ作業を集めた「逐次的凝集」まで来ると、まとまりはかなり濃くなります。もっとも高いのが、モジュール全体が1つの明確な目的のためだけに存在する「機能的凝集」です。

凝集度が低いモジュールは、変更理由の異なるコードが同居しているモジュールでもあります。決済ロジックとメール文面生成が同じクラスにあれば、メールの文言変更のたびに決済コードを巻き込んだテストとリリースが必要になります。「このモジュールが変更される理由は1つに絞れているか」という問いは、後述する SOLID原則の単一責任の原則へそのまま受け継がれました。

変更の波及範囲という見方

結合度と凝集度は独立した2軸ではなく、「変更がどこまで波及するか」という1つの関心を、モジュールの外側と内側から見たものです。密結合・低凝集なシステムでは、依存の網の目を伝って変更が全体へ広がります。疎結合・高凝集なシステムでは、変更は境界の内側で止まります。

密結合・低凝集疎結合・高凝集注文+通知変更決済+帳票在庫+認証共有データ依存が双方向に絡み、どの箱も変更理由を2つ抱える1か所の変更が4つの箱すべてへ波及する注文変更決済在庫通知公開インターフェース矢印: 使う側 → 使われる側注文の中身を変えても、決済・在庫・通知は巻き込まれない
変更の波及範囲の比較 — 疎結合・高凝集な設計では、依存が細い境界越しに一方向へ向き、変更が箱の内側に留まる

図の右側で矢印が一方向にしか向いていないことにも注目してください。注文は決済と在庫を「使う」けれど、決済と在庫は注文のことを何も知りません。依存の向きが揃っていれば、変更がどちらへ伝わるかも予測できます。この「依存の向きを意識して設計する」考え方は、SOLID原則の依存性逆転の原則や依存性注入へとつながっていきます。

同じことを手で確かめてください。下の部品では、8つの関数が4つのモジュールにばらばらに入っています。カードを押すとその関数を変更し、変更はその関数を直接呼んでいる関数へ届きます。モジュールは丸ごと再テスト・再リリースされる単位なので、光った箱の中身はすべて巻き込まれます。カードをつかんで箱から箱へ運ぶと、箱をまたぐ線(結合)と箱の中の線(凝集)がその場で数え直されます。

⚡ 体験: 関数を、モジュールに振り分ける
モジュールA島 2つ(バラバラ)モジュールB島 3つ(バラバラ)モジュールC島 2つ(バラバラ)モジュールD1関数注文受付注文確定決済処理返金在庫引当在庫補充メール送信通知文言
箱をまたぐ呼び出し(結合)箱の中の呼び出し(凝集)変更の波及
箱をまたぐ線8本箱の中の線0本バラバラな箱3直前の変更で再テスト—

関数のカードが4つのモジュールにばらばらに入っています。まず「通知文言」のカードを押して、文言を変更してみてください。変更がどの箱まで届くかが光ります。

疎結合にするための道具立て

疎結合を実現する手段は、規模ごとに用意されています。クラス単位では、具体的な実装ではなくインターフェース(抽象)に依存させ、実物は依存性注入で外から渡します。プロセスやサービスの単位では、内部実装を隠して API だけを公開し、さらにメッセージキューを挟んで非同期にすれば、相手の稼働状況からも切り離せます。デザインパターンの多く(Observer、Adapter、Facade など)も、本質的には結合を細くするための定石です。

落とし穴 — 疎結合は無料ではない

注意したいのは、「分割すれば疎結合になる」わけではないことです。モノリスを安易にサービス分割した結果、密結合のままネットワーク越しに切り離された「分散モノリス」— 1つの機能変更のために複数サービスの同時リリースが必要な状態 — は、この失敗の典型です。物理的な分割は結合を切るのではなく、結合のコストをネットワーク障害や運用複雑性という形で高くつくものに変えてしまいます。分割の前に、まず論理的な境界(何と何を一緒に変更するか)を見極めることが先です。

また、疎結合の追求にも限度があります。抽象層を挟むほど間接参照が増え、コードを追う手間は増えます。結合度と凝集度は「常に最大化・最小化する目標」ではなく、変更の見込みに応じて投資配分を決めるための物差しです。この物差しを個々のクラス設計の指針に落とし込んだものが SOLID原則であり、悪化した結合・凝集を継続的に直していく営みがリファクタリング、放置した結果積み上がるコストが技術的負債です。

試問

通販システムの8つの関数(注文受付・注文確定・決済処理・返金・在庫引当・在庫補充・メール送信・通知文言)が、4つのモジュールの箱にばらばらに入っています。矢印は「どの関数がどれを呼ぶか」です。カードをつかんで別の箱へ運べ(1箱に4つまで)、カードを押すとその関数を変更したときに再テストになる範囲が光ります。

✦ 命題挑戦中

関数のカードを箱へ運び直して、箱をまたぐ呼び出しを減らしつつ、どの箱も一緒に変わる関数だけのまとまりにせよ

  • 箱をまたぐ線が 4本以下(いま 8本)
  • どの箱も、中の関数が線でひとつながり(バラバラな箱が 0)
  • 「通知文言」を変えても、再テストが1つの箱で済む
⚡ 体験: 関数を、モジュールに振り分ける
モジュールA島 2つ(バラバラ)モジュールB島 3つ(バラバラ)モジュールC島 2つ(バラバラ)モジュールD1関数注文受付注文確定決済処理返金在庫引当在庫補充メール送信通知文言
箱をまたぐ呼び出し(結合)箱の中の呼び出し(凝集)変更の波及
箱をまたぐ線8本箱の中の線0本バラバラな箱3直前の変更で再テスト—

関数のカードが4つのモジュールにばらばらに入っています。まず「通知文言」のカードを押して、文言を変更してみてください。変更がどの箱まで届くかが光ります。