🏘️

パッケージ原則が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】安定依存の原則 〜レシピはフライパンに依存するな〜

原則:「パッケージの依存の向きは常に安定度の高いパッケージに向かっているべき」

つまり、パッケージはそのパッケージよりも変更の頻度が低いパッケージに依存するようにしようという話です。

これは、クリーンアーキテクチャの章で学んだクリーンアーキテクチャのレイヤー構造の関係性そのものを話しています。

詳しくはクリーンアーキテクチャの章を読んでください

https://zenn.dev/hashidev/articles/d5b3718b9c4041

特に、上記の記事で述べている「レシピとフライパンの関係」の部分を理解していればバッチリです。

【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(再利用・リリース等価の原則)**の視点も必要になってきます。

https://zenn.dev/hashidev/articles/2fec867ff70d62

つまり、そのライブラリが、業界標準(デファクトスタンダード)であり、バージョン管理がしっかりしていて、めったに破壊的変更が起きない(=安定している)と判断できる場合にのみ、「安定した抽象」として依存することが許されます。

言い換えると、適切なパッケージとして再利用可能な状態のサードパーティであるかどうかが重要になってくるということです。

アーキテクチャの「偉大な原則」

これで、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