結合度(Coupling)は、アプリケーションのあるモジュールが別のモジュールにどれだけ依存しているかを示す指標です。Wikipediaによると、弱い結合(low coupling)は適切に設計されたシステムの兆候であり、モジュールを隣接するモジュールを壊すことなく変更できます。モバイルアプリケーションを設計する際、結合度の管理はアーキテクトの主要なタスクの一つです。
重要なポイント
結合度(Coupling)は、あるモジュールやクラスが別のモジュールとどれだけ強く結びついているかを示す指標です。あるモジュールが別のモジュールの内部構造について知っていることが多ければ多いほど、結合度は高くなり、システムの変更は難しくなります。適切に設計されたアーキテクチャでは、結合度は最小限に抑えられるべきです — モジュールは厳密に定義されたインターフェースを介してのみ相互作用します。
結合度には2つの側面があります:アフェレント(入力依存性 — どれだけのモジュールがこれに依存しているか)とエフェレント(出力依存性 — これがどれだけのモジュールに依存しているか)です。これらの指標を分析することで、あるモジュールの変更が他の多くのモジュールに影響を与えるアーキテクチャのホットスポットを特定できます。IntelliJ Dependency AnalyzerやXcode Graphなどのツールがこれらの接続を可視化します。
ゼロ結合度は不可能であることを理解することが重要です — モジュールは何らかの方法で相互作用する必要があり、そうでなければシステムではなく孤立したプログラムの集合になります。アーキテクトの任務は、結合度を管理可能で透過的にすることです。理想的な状態:モジュールはインターフェースを介してのみ相互作用し、単純なデータのみを渡し、お互いの内部構造を知りません。これを弱い結合(loose coupling)と呼びます。
6つのタイプの結合度が、最良から最悪までのスケールを形成します。このスケールを理解することで、既存のコードを評価し、リファクタリングの方向性を選択するのに役立ちます。ほとんどのモバイルプロジェクトには混合タイプの結合度が存在し、アーキテクトの任務は強いタイプを弱いタイプに段階的に置き換えることです。
Data coupling(データ結合)— モジュールはメソッドパラメータを通じて単純なデータのみを交換します。モジュールAがモジュールBのメソッドを呼び出し、プリミティブや単純な構造を渡して結果を受け取ります。モジュールAはBが内部でどのように実装されているかを知りません。これが最も望ましい結合度のタイプです:変更の影響を最小限に抑えます。
例:EmailValidator.isValid(email: String): Boolean。消費者クラスは文字列を渡してブール値を受け取り、バリデーター内部の正規表現や検証ルールについて何も知りません。検証ロジックを変更しても消費者を変更する必要はありません — 結合度は最小限です。Data couplingはアプリケーションのすべてのパブリックインターフェースの目標です。
Stamp coupling(スタンプ結合)— モジュールは複合オブジェクトを交換しますが、そのフィールドの一部のみを使用します。モジュールAはcalculateDiscountメソッドにUserオブジェクトを渡し、そのメソッドはuser.statusのみを使用します。問題:User構造が変更されると(必須フィールドが追加される)、calculateDiscountモジュールは変更されませんが、Userオブジェクトを作成する消費者は変更されます。
実際には、スタンプ結合は避けられず、渡されるオブジェクトが標準データモデル(Entity)であれば許容可能です。問題は、モジュールが1つのフィールドのためにオブジェクト全体を受け取る場合に発生します。そのような場合は、特定の値を直接渡す方が良いです(data coupling)。解決策は、受け取り側によるフィールド使用状況を分析することです。
Control coupling(制御結合)— あるモジュールが別のモジュールに動作を制御するフラグを渡します(calculate(useNewAlgorithm: Boolean))。これはスタンプ結合よりも悪いです。なぜなら、消費者モジュールが呼び出されるモジュールの内部動作バリエーションを知る必要があるからです。解決策:メソッドを2つに分割します — calculateWithNewAlgorithm()とcalculateWithLegacyAlgorithm()。
External coupling(外部結合)— モジュールが外部プロトコル、データ形式、またはAPIに依存します。同じJSONを解析したり同じデータベースで動作するすべてのモジュールは外部結合を持ちます。完全に避けることはできませんが、隔離することはできます:外部形式と内部モデルの間にマッピング層を作成します。Common coupling(共通結合)— モジュールが共通のグローバル状態を共有します。Content coupling(内容結合)— 最悪のタイプで、モジュールが別のモジュールの内部データを直接変更します。
| 結合タイプ | レベル | 説明 |
|---|---|---|
| Data | 最良 | パラメータを通じた単純データの受け渡し |
| Stamp | 許容可能 | 部分使用を伴うオブジェクトの受け渡し |
| Control | 中程度 | フラグによる動作制御 |
| External | 高い | 外部プロトコル/形式への依存 |
| Common | 非常に高い | グローバル状態の共有 |
| Content | 許容不可 | モジュール内部データの直接変更 |
data(理想)からcontent(災害)までの結合度スケールはコードレビューの実用的なツールです。プロジェクトでcommonやcontent結合を見つけたら — それらはリファクタリングの優先目標です。Dataとstamp結合は許容可能でどのプロジェクトにも存在しますが、その量は管理されるべきです。
高い結合度は開発を遅いプロセスに変え、すべての変更で数十の潜在的に壊れるモジュールをチェックする必要があります。これはモバイル開発において特に重要です:プラットフォームは毎年(Android API Level、iOS SDK)、ライブラリは四半期ごと、ビジネス要件は継続的に更新されます。弱い結合だけが、絶え間ない回帰なしにこの変更の流れに対処する方法です。
実践例:すべての画面が直接NetworkingManagerとDatabaseManagerをインポートしているモバイルアプリ。HTTPクライアントをRetrofitからKtor(Android)またはURLSessionからAlamofire(iOS)に置き換える場合、開発者はすべての画面を修正する必要があります。低い結合度では、NetworkDataSourceインターフェースの背後に隠れた1つの実装を変更するだけで十分です — 消費者は置き換えに気づきません。
単体テストへの結合度の影響も計り知れません。高い結合度を持つクラス(コンストラクタで直接依存関係を作成する)は、分離してテストできません — データベース、ネットワーク、UIを引きずります。そのようなクラスをテストするには、エミュレータを起動して統合テストを待つ必要があります。低い結合度を持つクラスはコンストラクタインジェクションを通じて依存関係を受け入れ、簡単にモック化できます。
// 高い結合度 — クラスが自身で依存関係を作成する
class ProfileViewModelHigh {
private val api = RetrofitApi()
private val db = RoomDatabase.getInstance()
private val cache = MemoryCache()
}
// 低い結合度 — 依存関係がコンストラクタを通じて渡される
class ProfileViewModelLow(
private val api: ApiService,
private val db: DatabaseService,
private val cache: CacheService
)
最初のケースでは、ProfileViewModelHighが特定の実装に強く結びついています — RetrofitをKtorに置き換えるにはViewModelコードの変更が必要です。2番目のケースでは、ProfileViewModelLowはインターフェースにのみ依存し、その実装は外部から提供されます。2番目のクラスのテストは簡単です:モック実装を渡して、エミュレータなしでロジックを検証します。
依存性逆転の原則(SOLIDのD)は結合度削減の基盤です。この原則は、具象実装ではなく抽象化に依存するよう指示します。クラスが直接RetrofitApiオブジェクトを作成する代わりに、ApiServiceインターフェースを受け取るべきです。これにより、依存関係が特定のライブラリから抽象化レベルに移行し、消費者を変更せずに置き換え可能になります。
Observerパターン(またはそのリアクティブ版 — StateFlow、Combine Publishers)は、データソースと購読者の間の結合度を低減します。購読者はデータがどこから来るかを知らず — 単に変更に反応します。これにより送信者と受信者が分離され:既存の購読者を変更せずに新しいデータソースを追加できます。EventBusとSharedFlowも同じ原理で動作します。
Bridgeパターンは抽象化と実装を分離し、それらが独立して変更できるようにします。モバイル開発では、Bridgeは例えばプラットフォーム依存モジュールに使用されます:iOS(Kingfisher、Nuke)とAndroid(Glide、Coil)用の異なる実装を持つ共通のImageLoaderインターフェース。ImageLoaderを使用するコードは選択されたライブラリに依存せず、実装を変更するだけで置き換えられます。
依存性注入(DI)はモバイル開発における結合度削減の最も実用的なツールです。クラスが自身で依存関係を作成する代わりに、DIコンテナ(Android用のHilt、Koin、Dagger、iOS用のSwinject、Factory)が外部から依存関係を提供します。クラスはコンストラクタ、メソッド、またはプロパティインジェクションを通じて依存関係を受け取り、具象実装について知ることはありません。
DIはクラスの依存関係を明示的に文書化します:コンストラクタを見るだけで、クラスがどのモジュールと相互作用しているかがわかります。コンストラクタが異なる層から8つのパラメータを受け入れる場合 — これは過剰な結合度のシグナルであり、リファクタリングが必要です。目安はクラスあたり3〜4個までの依存関係です。それ以上は単一責任の原則違反と過剰な結合度を示します。
DIはテスティングも簡素化します:各テストでモック依存関係を持つクラスを作成し、実際のデータベースやネットワークを必要としません。FlutterではDIはProvider、Riverpod、GetItを通じて実装されます。フレームワークに関係なく、目標は一つ:依存関係を明示的で置き換え可能にすることでモジュール間の結合度を低減することです。モバイルプロジェクトでのDIの使用は2020年代から事実上の標準です。
// DIコンテナが依存関係グラフを構築する
protocol AuthServiceProtocol {
func login(email: String, password: String) async throws -> User
}
final class AuthService: AuthServiceProtocol {
func login(email: String, password: String) async throws -> User {
// 実装
}
}
// ViewModelは特定のサービスを知らない — プロトコルのみ
final class LoginViewModel {
private let auth: AuthServiceProtocol
init(auth: AuthServiceProtocol) {
self.auth = auth
}
}
// DI Containerは具象型が作成される唯一の場所
final class DIContainer {
lazy var authService: AuthServiceProtocol = AuthService()
lazy var loginViewModel: LoginViewModel {
LoginViewModel(auth: self.authService)
}
}
ここで、LoginViewModelは特定のAuthServiceではなく、AuthServiceProtocolプロトコルにのみ依存しています。実装の置き換え(例えばFirebase Authからカスタムサーバーへの移行)にはDIContainerの変更のみが必要です。AuthServiceProtocolのすべての消費者は影響を受けません — 抽象化とDIによって結合度は最小限に抑えられています。
よくある質問
凝集度はモジュール内部の一貫性を測定し、結合度はモジュール間の外部結合度を測定します。良いアーキテクチャは高い凝集度と低い結合度を目指します。これらの指標は反比例します:凝集度を上げると通常結合度は下がり、その逆も同様です。
Dataとstampは正常でどんなプロジェクトにも存在します。Control結合は限定的なシナリオ(例えばstrategyパターン)で許容されます。External結合は外部APIを扱う際に避けられませんが、マッピング層の背後に隔離されるべきです。Commonとcontent結合はアーキテクチャ上の問題の兆候であり、即時リファクタリングが必要です。
静的解析ツール:IntelliJ IDEA Dependency Matrix、Xcode Graph、Gradle Dependenciesレポート、SonarQube。指標:アフェレント結合度(Ca)、エフェレント結合度(Ce)、不安定性(Ce/(Ca+Ce))。高い不安定性(1に近い)は、モジュールが変更しやすく、参照するものが少ないことを意味します — これは良いことです。
極端に低い結合度は、コードのナビゲーションを複雑にする過剰な数の抽象化とインターフェースを意味する可能性があります。すべてのクラスに個別のインターフェースが作成されると、プログラマーはファイル間の移動に時間を浪費します。バランス:モジュールの外部APIにはインターフェースを、しかし内部のヘルパークラスすべてには不要です。
Strangler Figテクニックを使用します — 直接呼び出しを徐々にインターフェースに置き換えます。最も頻繁に参照されるクラスのインターフェース抽出から始めます。次にDIコンテナを導入します。隔離されたコードを特性テストでカバーして、リファクタリングがシステムの動作を変更しないことを確認します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。