前回は、モジュール化を「一緒に変わるものをまとめて境界を作ること」と整理しました。
しかし、モジュールを分割してから本当の問題が始まります。分けた断片同士が参照し始めるからです。
AがBを使い、BがCを使い、ある日Cが再びAを使う。その瞬間、モジュール化の利点はすべて消えます。3つが実質的に1つの塊になるからです。
結論から言えば、モジュール依存関係の原則は2つです。依存方向を一方向にし、変更頻度の高い側が安定した側に依存するようにします。
この記事では、依存方向の決め方、循環依存が生じる典型的な経路、断ち切る3つの方法を整理します。
依存方向はどちらに流すべきか
依存関係には方向があります。モジュールAがモジュールBをimportすると、AはBに依存します。
矢印をどちらに向けるべきかには、古典的な答えがあります。
不安定なものは安定したものに依存させる。逆にすると、小さな変更がシステム全体に波及します。
安定したモジュールとは、頻繁には変わらないモジュールです。ドメインモデルや共通プロトコルなどが該当します。
不安定なモジュールは頻繁に変わるモジュールです。画面(UI)や、要件に応じて手を入れ続けるイベント対応ロジックなどです。
そのため、健全な依存グラフはおおむね次の方向になります。
- 画面・機能モジュール → ドメインモジュール → 共通インターフェースモジュール
- 矢印は安定した側へ下向きにだけ流れ、上流へは戻りません。
Robert C. Martinはこれを安定依存の原則(SDP、Stable Dependencies Principle)と呼びました。
循環依存はなぜ生じるのか
循環依存は、悪意なくごく自然に生じることがほとんどです。典型的な経路は次のとおりです。
OrderモジュールがUserモジュールを参照します。注文に注文者情報が必要だからです。
やがて会員画面に「最近の注文一覧」を表示したいという要件が出ます。UserがOrderを参照した瞬間、循環が完成します。
循環が生じると、3つのものが崩れます。
- ビルド単位の分離に失敗:コンパイラーが別々に処理できず、実質的に1モジュールになります(Swiftはモジュール間の循環importをコンパイルエラーで禁止します)。
- 独立したテストができない:AのテストにはBが必要で、BのテストにはAが必要になるデッドロックです。
- 影響範囲を予測できない:どちらを直しても反対側まで再確認が必要になります。
循環依存はどう断ち切るか
実務で使う方法は大きく3つあります。
1つ目は、共通部分を下位へ移すことです。
双方が必要としているのが実は「相手の一部」なら、その部分をより安定した下位モジュールへ抽出します。OrderとUserが必要とする型だけをドメインモデルモジュールへ移すイメージです。
2つ目は、インターフェースで方向を逆転すること(DIP)です。
一方の依存をプロトコルに置き換えます。Userモジュールは「注文一覧を提供する何か」というプロトコルだけを定義し、実装はOrderモジュールが担います。
// User モジュール:必要な能力だけをプロトコルとして宣言
public protocol OrderHistoryProviding {
func recentOrders(of userID: String) -> [OrderSummary]
}
// Order モジュール: User モジュールのプロトコルを採用して実装
public struct OrderHistoryProvider: OrderHistoryProviding {
public func recentOrders(of userID: String) -> [OrderSummary] {
// 注文リポジトリを検索
}
}
これで矢印はOrder → Userの一方向だけになります。前回のDIP記事で扱った依存性逆転が、モジュール単位でそのまま使われています。
3つ目は、組み立てを上位へ移すことです。
2つのモジュールを直接つながず、両方を知る上位モジュール(アプリターゲットや組み立て層)が接続します。画面遷移など、機能モジュール同士が互いを呼ぶ必要がある場面で特に有効です。
| 状況 | 断ち切る方法 |
|---|---|
| 互いの型の一部だけが必要 | 共通型を下位モジュールへ抽出 |
| 一方が他方の機能を呼び出す | プロトコルを定義して依存性を逆転 |
| 機能モジュール間の画面遷移 | 上位の組み立て層で接続 |
いつ適用し、いつ過剰になるのか
依存方向の管理にもコストがかかります。プロトコルが増え、組み立てコードも必要になるからです。
- モジュールが3〜4個以上でチームも分かれているなら、方向のルールを文書化し、ツールで循環を監視する価値があります。
- 2モジュールの小規模プロジェクトなら、循環さえ作らなければ十分です。すべての参照をプロトコルで包むのは過剰です。
- すでに循環のあるレガシーなら、一度にすべてを断とうとせず、最も頻繁に変わるモジュールから矢印を整理しましょう。
面接ではこう聞かれます
Q. モジュール間の循環依存はなぜ問題で、どう解決しますか?
循環が生じると、ビルド・テスト・デプロイの観点で2つのモジュールが一体化し、モジュール化の利点が失われます。共通型を下位モジュールへ抽出する、プロトコルで依存方向を逆転する(DIP)、上位の組み立て層で2つを接続する、という方法で矢印を一方向に整理します。
Q. 安定依存の原則(SDP)を説明してください。
モジュールは、自分より安定した、つまり変更の少ないモジュールだけに依存すべきだという原則です。変更の多いモジュールが依存グラフの下流にあると、その変更が上流全体へ伝播するため、変更頻度の低いドメイン・インターフェースモジュールをグラフの下側に置く構造が健全です。
次回は、多くのチームがモジュール化の途中で陥る罠、名前からして危険なCommonモジュールについて扱います。
「とりあえず共通だからCommonに入れよう」がモジュール全体を再び1つの塊へ戻す仕組みと、レイヤー構造での予防策を解説します。

