パッケージ原則がOOPとアーキテクチャを繋ぐ #3~ ADP, SDP, SAP編~
はじめに:区画の「繋ぎ方」を学ぶ
前回は、CCPとCRPという原則を通して、「凝集度の高い」理想的な区画(パッケージ)の作り方を学びました。
しかし、優れた都市計画が「良い区画」だけで終わらないのと同じで、私たちの設計も 「区画と区画をどう繋ぐか」 というルールが極めて重要になってきます。
この「区画間の関係性」を定義するのが、今回学ぶ パッケージの「結合度」 に関する3つの原則です。
この議論は、これまで学んできた「結合度」と「安定度」の議論そのものであり、次章に控えるSOLID原則への完璧な橋渡しにもなります。
【ADP】非循環依存の原則 〜開発プロセスを麻痺させる「循環」〜
ADP(Acyclic Dependencies Principle)- 非循環依存の原則
原則: 「パッケージ間の依存関係に、循環(ループ)を作ってはならない。」
なぜでしょう?パッケージAがBに、BがAに依存すると、両者は事実上 「1つの巨大なパッケージ」 となり、分離した意味がなくなります。
循環の「本当の痛み」:開発プロセスの麻痺
循環依存の本当の恐ろしさは、実行エラーによってプログラムが動かなくなることよりも、開発・テスト・リリースのプロセスが完全に停滞することにあります。
1. ビルドシステムの破綻
CoreLogic(A)をビルドするにはDatabase(B)が、Database(B)をビルドするにはCoreLogic(A)が必要...
というように、「鶏と卵」の状態 に陥りビルドそのものが失敗します。
2. テストの崩壊
CoreLogic(A)の単体テストをしたいだけなのに、依存先のDatabase(B)全体を読み込む必要があります。
AとBは事実上 「一枚岩(モノリス)」 と化しており、もはや「単体」テストとは呼べません。
3. 二日酔い症候群
開発者AがCoreLogicを、開発者BがDatabaseを、それぞれローカルで開発・テストし、問題なく動作したとします。
しかし、翌朝2人がコードをチェックイン(統合)した瞬間、お互いの変更が干渉し、システム全体が壊れます。
これが、ADPが引き起こす「機能不全」の正体です。
どうやって「循環」を断ち切るか?
では、この循環をどう解決すればよいでしょうか?
「とにかく循環をなくせ」と闇雲に修正する前に、「なぜ循環が起きたのか?」を診断することが必須です。
その診断結果によって、治療に必要な「処方箋」は以下のように大きく異なってきます。
診断①:高い凝集度のパッケージ関係(CCP違反)
😷症状: よく見たらCoreLogicとDatabaseは、同じ理由で同時に変更されることが非常に多い
🩺診断: 本来1つのパッケージにすべきもの(高凝集)を間違って2つに分離してしまったという、CCP(共通閉鎖の原則)違反によるもの
💊処方箋: 2つのパッケージを、1つの「区画」に統合します。 パッケージ内での循環は何も問題ありません
診断②:低い凝集度のパッケージ関係(本物のADP違反)
😷症状: UseCase(ビジネスロジック)とPresenter(UIロジック)が循環してしまう。
- UseCase → Presenter(処理結果を渡したい)
- Presenter → UseCase(ユーザーの操作を伝えたい)
🩺診断: この2つは、「変更の理由」も「タイミング」も全く異なります(ビジネスルール vs UIデザイン)。1つのパッケージに統合すると、CCPに違反します。
💊処方箋: 第3章で学んだポリモーフィズムの「力」を使います。 UseCaseがPresenter(具象)に依存するのではなく UseCaseが定義したインターフェース(抽象)にPresenterが依存するように依存関係を「逆転」させます。
このテクニックこそが、次章で学ぶ SOLID原則の「DIP(依存性逆転の原則)」 です。 (※詳細はSOLIDの章で深掘りします)
ADPからSDPへ
ADPの議論から、私たちは以下の2つの重要な結論を得ました。
パッケージ間の依存は、 「循環」 してはならない。
依存の向きは 「抽象(Interface)」 に向けることで、クリーンに制御できる。
パッケージの依存は、常に一方向であるべきです。
では、その「一方向」とは、どちらに向けるべきなのでしょうか? 「安定」した方へ向けるべきか、「不安定」な方へ向けるべきか?
この依存の 「方向性」を定義する、次の原則がSDP(安定依存の原則) です。
【SDP】安定依存の原則 〜レシピはフライパンに依存するな〜
原則:「パッケージの依存の向きは常に安定度の高いパッケージに向かっているべき」
つまり、パッケージはそのパッケージよりも変更の頻度が低いパッケージに依存するようにしようという話です。
これは、クリーンアーキテクチャの章で学んだクリーンアーキテクチャのレイヤー構造の関係性そのものを話しています。
詳しくはクリーンアーキテクチャの章を読んでください
特に、上記の記事で述べている「レシピとフライパンの関係」の部分を理解していればバッチリです。


【SAP】「抽象」こそが「安定」である
原則:「パッケージは、「抽象的」であるほど「安定」する。 (The Stable Abstractions Principle)」
SDPは、私たちに「安定なものに依存しろ」という『方向性』を示してくれました。
しかし「どうすればパッケージを安定』にできるのか?」という「具体的な方法」は、まだ示されていません。
上記の疑問を、「抽象度」と「安定度」の関係性に基づいて解消するのが SAP(安定抽象の原則) なのです。
OOPの章で学んだことを思い出してください。
DogやCatといった「具象クラス」は、新しい動物が追加されるたびに変更・追加される「不安定」な存在でした。
しかし、それらの汎化であるPetという「インターフェース(抽象)」は、新しいペットが増えても変わることがない、非常に「安定した」存在でした。
では、パッケージ設計において「抽象」と見なされるのは、interfaceだけなのでしょうか?
いいえ。
SAPが言う「抽象」とは 変更の理由が少ないもの全般 を指し、それらは大きく2つのグループに分けられます。
抽象グループ①:高レベルな「方針(ポリシー)」
「ビジネスの『ルール』は決まっているが、『詳細』は決まっていない」 という種類の抽象です。
例❶:インターフェース
Petインターフェースは、「ペットは鳴くものである(Speak())」という 「方針」 を定義します。
「どう鳴くか("Meow", "Woof")」という 「詳細(具象)」 は定義しません。
例❷:詳細を実装しないロジック(テンプレートメソッドパターン)
「年齢に応じて入場料を変更する」というロジックを考えてみましょう。
安定したbillingパッケージが、「①年齢を受け取る → ②料金を計算する → ③請求書を発行する」という 「大まかな流れ(方針)」 を定義します。
一方で、「② 料金を計算する」という 具体的な計算ロジック(if age < 18...)は、不安定なfee-rulesパッケージに「移譲(DIP)」 します。
このbillingパッケージは、方針だけを定義し、詳細(具象)を持たないため、非常に「抽象的」で「安定」していると言えます。
抽象グループ②:汎用的な「概念(ツール)」
「特定のビジネス(例:ペットショップ)に依存しない、汎用的なもの」 という種類の抽象です。
例❶:プログラミング言語と標準ライブラリ
string型、timeパッケージ、io.Readerインターフェースなどを考えてみましょう。
これらは、あなたが開発している「ペットショップ」のロジックがどう変わろうと、一切影響を受けません。
これらは、最も汎用的であり、最も変更の理由が少ないため、 最も「抽象的」で「安定した」 パッケージと見なされます。
例❷:業界標準のサードパーティライブラリ(注意点あり)
標準ライブラリのlog機能は、上記の理由で間違いなく「安定した抽象」です。
ただし、サードパーティのライブラリ(例:Logrus)を「安定」と見なすかは、**REP(再利用・リリース等価の原則)**の視点も必要になってきます。
つまり、そのライブラリが、業界標準(デファクトスタンダード)であり、バージョン管理がしっかりしていて、めったに破壊的変更が起きない(=安定している)と判断できる場合にのみ、「安定した抽象」として依存することが許されます。
言い換えると、適切なパッケージとして再利用可能な状態のサードパーティであるかどうかが重要になってくるということです。
アーキテクチャの「偉大な原則」
これで、3つの原則の主張が一つに繋がりました。
ADP:「依存は 一方向 にしろ(循環させるな)」
SDP:「その方向は 『安定』 な方へ向かえ」
SAP:「そして 『抽象』 こそが『安定』である」
この3つの原則を組み合わせると、クリーンアーキテクチャとOOPを貫く、あの偉大な原則が導き出されます。
依存は、常に「抽象」に対して行え (Depend upon Abstractions)
これこそが、次章で学ぶSOLID原則の「DIP(依存性逆転の原則)」の正体であり、私たちが目指すべき、変更に強いシステムの最終的な答えなのです。
パッケージ原則のまとめ
これまで、パッケージ原則の意義と大まかに3つの側面に沿ってパッケージ原則を3つの記事を通じて学んできました。
1つ目の側面
「区画整理(パッケージ原則)」を通じて、OOPのクラスをアーキテクチャのレイヤーへと正しく配置する意義とそのためのパッケージの方針(REP)を学びました。
2つ目の側面
凝集度(REP, CCP, CRP) が、変更しやすいパッケージの作り方を教えてくれました。
3つ目の側面
結合度(ADP, SDP, SAP) が、依存の矢印を「安定した抽象(インターフェース)」へ向けるべき理由を教えてくれました。
SOLIDへの「問い」:
これで、私たちの設計の「目的地」は明確になりました。
「インターフェースに依存し、循環のない、安定した構造」です。
しかし、どうすれば、日々のクラス設計(OOP)で、そのような構造を自然に生み出すことができるのでしょうか?
その問いに答える、クラスレベルの5つの具体的な「型」、それこそがSOLID原則です。
次回、この偉大な原則を一つずつ解き明かしていきます。
Discussion