ソフトウェア設計

【モジュール化 #3】Commonモジュールがごみ箱になる理由

モジュール化を進めるチームの多くは、いずれCommon、Core、Utilsのような名前のモジュールを作ります。

読了 5 分
【モジュール化 #3】Commonモジュールがごみ箱になる理由のカバー画像

モジュール化を進めるチームの多くは、いずれCommon、Core、Utilsのような名前のモジュールを作ります。

「複数箇所で使うから、とりあえずここに」という判断を重ねると、いつの間にかそのモジュールはプロジェクト最大で、最も頻繁に変更され、全員が依存する存在になります。

結論から言えば、Commonという名前のモジュールは、凝集度がないことの告白です。「共通」はコードをまとめる基準にはなりません。

この記事では、Commonモジュールがなぜモジュール化を後退させるのか、また予防策となるレイヤー構造(垂直分割)と機能構造(水平分割)を整理します。


Commonモジュールはなぜ危険なのか?

問題は3段階で進みます。

第1段階は、ごみ箱化です。「置き場所が曖昧なコード」がすべてCommonに入ります。日付フォーマッターの隣にネットワーククライアント、その隣にカスタムボタンが置かれます。互いに何の関係もありません。

第2段階は、全員が依存する状態です。すべての機能モジュールがCommonをimportします。この時点でCommonはグラフ上で最も下流、つまり最も安定しているべき位置を占めます。

第3段階は、全体の再ビルドです。しかしCommonはごみ箱なので、最も頻繁に変更されます。前回の記事の安定依存の原則が完全に逆転した状態です。ボタンの色を1つ変えただけで、全モジュールが再ビルドされます。

最も多く依存されるモジュールが最も頻繁に変わるモジュールになった瞬間、モジュール境界はあってないようなものになります。


レイヤー構造:垂直に分ける

予防策の第一の軸は、役割に応じた垂直分割です。よく使われる3〜4層構成は次のとおりです。

レイヤー 含めるもの
Feature 画面・機能単位のモジュール ホーム、検索、注文、設定
Domain 業務ルール・モデル・ユースケース 注文ポリシー、会員モデル
Core 特定技術の薄いラッパー ネットワーク、ストレージ、ロギング
Shared 本当に汎用的なユーティリティ 日付フォーマッター、文字列拡張

ルールは1つだけです。依存は上から下へ一方向にのみ流します。

FeatureはDomainとCoreを使えますが、CoreがFeatureを知ってはいけません。Feature同士も直接参照せず、上位のアプリターゲットで組み立てます。

依存は下方向にのみ流れ、レイヤーのルールはこの図にすべて表れています。
依存は下方向にのみ流れ、レイヤーのルールはこの図にすべて表れています。

ここで重要なのは、Commonとの違いです。Coreは巨大な1つのモジュールではなく、技術単位に分割した複数のモジュールです。

  • CoreNetwork、CoreStorage、CoreLoggingのように、それぞれ独立したモジュールにします。
  • 検索機能がロギングだけを使うなら、importするのはCoreLoggingだけです。

これならネットワークコードを修正しても、ロギングだけを使うモジュールは再ビルドされません。


機能構造:水平に分ける

第二の軸は、ドメインに応じた水平分割です。同じレイヤー内でも、ホーム・検索・注文は別々のモジュールであるべきです。

判断基準は第1回で扱った問いです。「これらのコードは同じ理由で変更されるのか?」

  • 検索ポリシーを変更しても注文コードを変更する必要がないなら、両者は別モジュールです。
  • 注文画面と注文ユースケースが常に一緒に変わるなら、さらに垂直分割するのは過剰かもしれません。

垂直(レイヤー)と水平(機能)を重ねると、グリッドになります。実際のモジュールは「注文機能のDomain」「検索機能のFeature」のように、グリッドの1マスになります。


Commonがすでに肥大化していたら?

既存のCommonを一度に壊す必要はありません。実務で検証された順序は次のとおりです。

  1. 新規追加禁止:今日からCommonに新しいコードを入れないルールをまず定めます。
  2. 利用箇所の調査:最も頻繁に変わるコードから、実際にどこで使われているか確認します。
  3. 居場所を決める:1つの機能だけで使うならその機能モジュールへ、特定技術のラッパーならCore系モジュールへ移します。
  4. 残りを改名:最後まで残る本当に汎用的なコードだけを、Sharedのような狭い名前で隔離します。

数か月かかる作業ですが、段階ごとに再ビルド範囲が目に見えて縮小することを確認できます。

整理は、新しいものを入れないことから始まります。
整理は、新しいものを入れないことから始まります。

この構造はいつ必要で、いつ過剰なのか?

  • 開発者が4〜5人以上で、機能領域が3つ以上なら、レイヤーのルールを定める価値は十分にあります。
  • 1〜2人のプロジェクトなら、まずはFeature/Sharedの2層で十分です。最初からグリッドを描くのは過剰です。
  • 規模を問わず、「Common・Utilsという名前のモジュールを作らない」というルールだけは最初から守ることを勧めます。

面接ではこう質問します

Q. 共通モジュール(Common)が大きくなるとどんな問題が起き、どう設計するのがよいでしょうか?

全員が依存するモジュールが最も頻繁に変わるモジュールになるため、小さな修正でも全体の再ビルドと広範囲な影響調査が必要になります。安定依存の原則が逆転した状態です。ネットワーク・ストレージ・ロギングのように役割ごとに細かく分け、本当に汎用的なコードだけを最小限のSharedモジュールに隔離します。

Q. モジュール構造をレイヤーに分ける際のルールを説明してください。

Feature、Domain、Coreのように役割ごとのレイヤーを設け、依存は上から下への一方向だけに許可します。同じレイヤーのモジュール同士は直接参照せず、上位の組み立て層で接続します。このルールを守れば、変更の影響範囲は常に上側だけに限定されます。


以上がモジュール化の原論3部作でした。境界、依存方向、レイヤー構造まで押さえれば、概念的な道具はそろったことになります。

次回からは、これらの原則をiOSプロジェクトにそのまま適用します。Swift Packageで実際にモジュールを分割する方法から始めます。