結合度
#設計 #ソフトウェアの品質
code:markdown]
チートシート
1. 内容結合:別モジュールの内部実装を直接参照・改変する
2. 共通結合:グローバル変数や共有状態を複数モジュールが読み書きする
3. 外部結合:外部フォーマットやプロトコルなどモジュール外の規約を共有する
4. 制御結合:フラグや制御パラメータで相手の実行パスを制御する
5. スタンプ結合:複合データ構造を渡すが、受け手はその一部しか使わない
6. データ結合:必要なプリミティブだけを引数で渡す
7. メッセージ結合:パラメータを持たないメッセージだけでやり取りする(引数のない関数の呼び出しで結合)
/icons/hr.icon
coupling
結合度は品質尺度である
結合度は設計の良し悪しを直接判定するものではなく、依存構造の状態を測定する品質尺度である。温度計が温度を変える手段ではないように、結合度は「この設計の依存関係はどういう状態か」を評価するが、それ自体は設計をどう変えるべきかを指示しない。ISO/IEC 25010のような品質モデルにおいては、保守性や変更容易性といった品質特性を間接的に測定するための内部品質属性として位置づけられる。結合度が高いという測定結果は保守性の低下を示唆するが、それをどう解釈し、どう対処するかは文脈に依存する。この区別を見失うと「結合度を下げること」が自己目的化し、インターフェースを大量に挟んで数値上の結合度は下がったが間接層が増えて認知負荷が上がり保守性はむしろ悪化する、という転倒が起こる。
結合は消せない、形を変えるだけである
結合度はモジュール間の依存関係の強さを表す設計指標である。「低結合・高凝集」は広く知られた原則だが、実務上重要なのは「結合度を下げること」そのものではなく、結合がどこにどんな形で存在しているかを認識し、意図的に配置することにある。
あるレイヤーの結合を弱めると、別のレイヤーに結合が移動するか、新たな運用上の結合が生まれる。インターフェースを抽出すればクラス間の直接依存は消えるが、その抽象に対する暗黙の期待が結合として残る。イベント駆動にすれば呼び出し側と受け手の直接依存はなくなるが、イベントスキーマとイベント順序に依存が移る。結合はゼロにはならない。どの形の結合を受け入れるかを選ぶのが設計判断である。
古典的な分類
構造化設計(Constantine, 1970s)では結合度を段階づけた。強い順に並べると以下のようになる。
1. 内容結合:別モジュールの内部実装を直接参照・改変する
2. 共通結合:グローバル変数や共有状態を複数モジュールが読み書きする
3. 外部結合:外部フォーマットやプロトコルなどモジュール外の規約を共有する
4. 制御結合:フラグや制御パラメータで相手の実行パスを制御する
5. スタンプ結合:複合データ構造を渡すが、受け手はその一部しか使わない
6. データ結合:必要なプリミティブだけを引数で渡す
7. メッセージ結合:パラメータを持たないメッセージだけでやり取りする(引数のない関数の呼び出しで結合)
1〜3は現代のコードベースでは意識的に避けられていることが多い。実務で見落としやすいのは4と5である。
制御結合は include_archived: true のようなbooleanフラグで相手の分岐を制御するパターンで頻出する。呼び出し側が相手の内部ロジックを知っている前提になっており、フラグが増えるたびに暗黙の結合が積み上がる。
スタンプ結合は巨大なRequestオブジェクトやContextオブジェクトを丸ごと渡しているケースで起こる。受け手が実際に何を使っているか分からなくなり、データ構造を変更する際の影響範囲が読めなくなる。
現代のソフトウェアにおける結合の次元
古典的な分類は関数やクラスのレベルの話だが、現代のソフトウェアではそれ以外の次元での結合がむしろ支配的になる。
時間的結合(Temporal Coupling)
処理の順序に暗黙の依存がある状態を指す。「Aを呼んでからBを呼ばないと正しく動かない」というケースで、初期化処理やセットアップの順序依存として頻出する。非同期処理やイベントソーシングはこの結合を緩和する手段として機能するが、代わりに結果整合性の複雑さを引き受ける。
空間的結合(Deployment Coupling)
同じプロセスやノードにデプロイされることを前提にしている状態を指す。モノリスでは自然にこの結合が存在する。マイクロサービスへの分割はこれを断つが、代わりにネットワーク越しの結合(レイテンシ、可用性の連鎖)を生む。空間的結合を解消すること自体がゴールではなく、デプロイ独立性がどれだけ必要かによって判断が変わる。
スキーマ結合
共有DBスキーマやAPIスキーマへの依存である。複数ドメインから参照されるマスターデータを持つシステムでは、ここが最も支配的な結合要因になることが多い。たとえば組織テーブルのカラムを1つ追加するだけで、参照する複数のサービスやバッチに変更が波及するような構造がこれにあたる。技術レイヤーでいくら疎結合にしても、スキーマ結合が残っていれば変更の波及は止められない。
実行時結合(Runtime Coupling)
同期呼び出しか非同期かという違いがもたらす結合である。同期呼び出しはレイテンシの伝搬と可用性の連鎖を生む。呼び出し先がダウンすれば呼び出し元も巻き込まれる。非同期にすれば実行時結合は弱まるが、処理結果の確認やエラーハンドリングの設計が複雑になる。
結合の形を変えるアプローチ
インターフェースの抽出は実装詳細への依存を断つ。ただし抽象層が増えるほど「この抽象の裏で何が起きているか」を追う認知負荷が上がる
イベント駆動は発行者と購読者の直接依存を排除する。ただし結果整合性、イベント順序の保証、デバッグの困難さを引き受ける
APIゲートウェイやBFFはクライアントとバックエンドの結合を緩和する。ただし運用対象が増え、ゲートウェイ自体がボトルネックになりうる
Packwerk等のモジュール境界はモノリス内で論理的境界を強制する。ただし既存の違反を解消するマイグレーションコストが発生する
CQRSやドメインイベントは読み書きの関心を分離する。ただしシステム全体の状態管理が複雑になる
いずれも結合を消すのではなく、結合の形を選び直す行為である。
/icons/hr.icon
ソフトウェアの品質(結合度)って?? #ソフトウェア工学 - Qiita