コンポーネントの原則とは
コンポーネントの原則(Component Principles)は、主に『Clean Architecture』の著者として知られるロバート・C・マーチン氏が提唱した、ソフトウェアを構成する「部品(コンポーネント)」をどのようにまとめ、どのように依存させるべきかを示した指針です。
コンポーネントの原則における「コンポーネント」とは、一言で言えばデプロイ可能な最小単位を指します。プログラミングにおける「クラス」や「関数」よりも一段階大きな括りであり、複数のモジュールをひとまとめにした「実行可能なバイナリファイル」や「ライブラリ」のことだと考えるとイメージしやすくなります。
これらの原則は、大きく分けてコンポーネントの凝集度とコンポーネントの結合という2つのカテゴリ、計6つの原則で構成されています。
この記事では、一つ一つの原則に触れながら、コンポーネントの原則を理解していきます。
コンポーネントの凝集度(Component Cohesion)
「どのクラスをどのコンポーネントに含めるべきか」を決めるための原則です。
- REP:再利用・リリース等価の原則 (Release/Reuse Equivalency Principle)
- CCP:閉鎖性共通の原則 (Common Closure Principle)
- CRP:全利用共通の原則 (Common Reuse Principle)
これら3つの原則は以下のようにトレードオフの関係にあります。
-
REP と CCP (コンポーネントを大きくする力)
「再利用しやすく(REP)」かつ「変更箇所を一箇所にまとめたい(CCP)」と考えると、関連する機能をどんどん一つのコンポーネントに詰め込むことになります。その結果、コンポーネントは巨大化します。 -
CRP (コンポーネントを小さくする力)
「利用者に不要なものを見せたくない(CRP)」と考えると、機能ごとにコンポーネントを細かく分割することになります。その結果、管理すべきコンポーネントの数が膨大になります。
REP:再利用・リリース等価の原則 (Release/Reuse Equivalency Principle)
REPとは、再利用の単位はリリースの単位と等しくなければならないという原則です。追跡可能なリリース番号やドキュメントがなければ、そのコンポーネントを安心して再利用することはできません。
例えば、共通処理があるとします。
utils
├ string.ts
├ date.ts
└ http.ts
これを他のプロジェクトで使う場合に、string.ts をコピーしてしまうケースがあるとします。
すると、
ProjectA
ProjectB
ProjectC
それぞれにstring.tsのコピーが存在することになってしまいます。
何が問題かと言うと、
-
バグ修正が共有されない
ProjectAでバグを修正してもProjectBとProjectCには反映されません。結果として、同じバグが何度も発生し、管理が困難になります。 -
バージョンが追跡できない
コピーしたコードには、バージョン、変更履歴、リリース情報がありません。つまり、どのコードが最新なのか分からないという状態になります。
REPでは、再利用コードをコンポーネントとしてリリースすることを推奨しています。
例えば、
utils-lib v1.2.0
このように、バージョン/リリース履歴/APIを持った形で提供します。
プロジェクトは、
projectA
projectB
projectC
から
utils-lib
を依存として利用します。
現代開発において、REPは多くのパッケージ管理システムで実現されています。
例:
JavaScript
npm install react
Python
pip install requests
Go
go get
これらはすべてライブラリ = リリース単位になっています。
CCP:閉鎖性共通の原則 (Common Closure Principle)
CCPとは、同じ理由で変更されるクラスは、同じコンポーネントにまとめるという原則です。SOLID原則の単一責任原則(SRP)をコンポーネントレベルに引き上げたもので、変更の影響範囲を最小限に抑えることを目的としています。
例えば、ECサイトにおいて次のクラスがあるとします。
Order
User
Payment
もしこれらが、
common-module
という1つのコンポーネントに入っていた場合、
例えば、注文仕様の変更が発生した場合にはOrderを変更しますが、common-module全体を再ビルドする必要があります。また、その変更はUserやPaymentを使っているシステムにも影響を及ぼす可能性もあります。
CCPに従う場合、変更理由ごとにコンポーネントを分けます。
order-module
├ Order
├ OrderService
└ OrderRepository
user-module
├ User
└ UserService
payment-module
├ Payment
└ PaymentService
もし注文仕様が変更した場合、order-moduleだけ変更すればよくなります。
CRP:全利用共通の原則 (Common Reuse Principle)
CRPとは、一緒に再利用されるクラスだけを同じコンポーネントに含めるという原則です。利用者が使わないクラスに依存することを避けるため、SOLID原則のインターフェース分離の原則(ISP)のコンポーネント版と言えます。
例えば、コンポーネントの中には以下のような複数のクラスが含まれていたとします。
utils-lib
├ StringUtil
├ DateUtil
├ HttpUtil
└ ImageUtil
もし利用者がStringUtilだけ使いたい場合でも、utils-libに依存するとDateUtil、HttpUtil、ImageUtilにも依存することになります。これを不要な依存関係(Unnecessary dependency)と言います。
CRPに従わない場合、次のような問題が起きます。
-
不要な依存が増える
プロジェクトがStringUtilしか使っていない場合でもutils-lib全体に依存します。するとDateUtilの変更でも再ビルドが必要になる可能性があります。 -
不要な変更の影響を受ける
例えば、HttpUtilにバグ修正が入ったとします。するとutils-lib v1.2.0に更新する必要があります。しかし、利用者はStringUtilしか使っていません。つまり、関係ない変更の影響を受けることになります。
CRPに従うと、コンポーネントを適切に分割する必要があります。
string-utils
date-utils
http-utils
image-utils
このようにすることで、利用者はstring-utilsだけに依存でき、使わない機能に依存する必要がなくなります。
コンポーネントの結合(Component Coupling)
「コンポーネント間の関係をどのように構築すべきか」を決めるための原則です。
- ADP:非循環依存関係の原則 (Acyclic Dependencies Principle)
- SDP:安定依存の原則 (Stable Dependencies Principle)
- SAP:安定抽象の原則 (Stable Abstractions Principle)
ADP:非循環依存関係の原則 (Acyclic Dependencies Principle)
ADPとは、コンポーネントの依存関係に循環を作ってはいけないいという原則です。
循環依存とは、コンポーネント同士がぐるっと依存し合う状態です。
A → B → C → A
上記は、
AはBに依存
BはCに依存
CはAに依存
これが循環依存(Cycle)です。循環があると、一つの変更がシステム全体に波及し、ビルドやリリースが困難になります。
ADPでは、依存関係を一方向にすることを推奨します。
UI
↓
Application
↓
Domain
↓
Infrastructure
依存は 上 → 下だけです。こうするとUI → Application → Domain → Infrastructureとなり循環が発生しません。
循環依存を解決する方法には、主に次の2つの方法があります。
方法① 依存関係逆転(DIP:Dependency Inversion Principle)
例えば、
A → B
B → A
の循環がある場合、
インターフェースを作ります。
A → Interface
B → Interface
こうすることで、
A
↓
Interface
↑
B
になり、循環がなくなります。
方法② コンポーネントの再分割
もし、
A → B
B → C
C → A
なら、共通部分を新しいコンポーネント(common)にします。
A → common
B → common
C → common
SDP:安定依存の原則 (Stable Dependencies Principle)
SDPとは、より安定した方向に向かって依存せよという原則です。ここでいう安定(Stable) とは「変更されにくい」ことを意味します。
SDPに従う場合、以下のように変更しにくいコンポーネントが、変更しやすい(不安定な)コンポーネントに依存してはいけません。
Database → BusinessLogic
もしBusinessLogicが変更されるとDatabaseも影響を受け、安定しているべき部分(Database)が壊れる可能性があります。
SDPでは、依存関係を次のようにします。
UI(不安定) → Application(中間) → Domain(安定)
上記では、依存関係が不安定 → 安定へ向かっています。このようにすることで、変更が発生しやすい部分の影響を、システム全体に広げないようにすることができます。
また、安定性は依存されている数で決まります。
例えば、
A ← B
A ← C
A ← D
この場合、Aは3つのコンポーネントに依存されています。Aを変更するとB、C、Dすべてに影響するため、Aは安定している(変更しにくいコンポーネント)と言えます。
SAP:安定抽象の原則 (Stable Abstractions Principle)
SAPとは、変更されにくいコンポーネントは、具体的な実装ではなく抽象(インターフェース)で構成するべきという原則です。
前の原則で紹介したSDP(安定依存の原則) では、不安定 → 安定の方向に依存するべきだと説明しました。
例えば、
UI → Application → Domain
です。
このとき問題になるのは安定したコンポーネントが具体的すぎる場合です。
もしDomainに次のような具体実装があるとします。
Domain
├ MySQLUserRepository
├ PostgreSQLUserRepository
しかし将来、
MySQL → PostgreSQL
DB → API
DB → NoSQL
のように変更したくなるかもしれません。
もしDomainが具体実装に依存していると、Domainに依存しているすべてのコードに影響を与えてしまうという問題が起きます。しかし、Domainは安定しているべき部分(変更しにくいコンポーネント)です。
そこで、SAPでは、安定したコンポーネントを抽象化します。
例えば、次のようにします。
Domain
├ UserRepository(interface)
実装は外側に置きます。
Infrastructure
├ MySQLUserRepository
├ PostgreSQLUserRepository
以下のようなイメージとなります。
┌─────────────────┐
│ Domain │
│ │
│ UserRepository │
│ (interface) │
└────────▲────────┘
│
│ 依存
│
┌───────────┴───────────┐
│ Infrastructure │
│ │
│ MySQLUserRepository │
│ PostgreSQLUserRepository│
└────────────────────────┘
こうすることで、将来NoSQLUserRepositoryの追加や外部システムの変更が発生しても、Domainを変更する必要がなくなり、システムの中心であるDomainを安定させたまま、外側の実装を柔軟に変更できる設計になります。
まとめ
コンポーネントの原則は、ソフトウェアをどのような単位で分割し、どのように依存関係を構築するべきかを示した設計指針です。これらの原則に従うことで、変更に強く、再利用しやすく、保守しやすいシステムを構築することができます。
これまで紹介してきたように、コンポーネントの原則は、大きく次の2つの観点から構成されています。
1. コンポーネントの凝集度(Cohesion)
どのクラスを同じコンポーネントにまとめるべきかを決める原則です。
- REP: 再利用する単位はリリース単位と一致させる
- CCP: 同じ理由で変更されるクラスをまとめる
- CRP: 一緒に利用されるクラスだけを同じコンポーネントに含める
これらは互いにトレードオフの関係にあり、再利用性・変更容易性・依存関係の最小化のバランスを取りながら設計することが重要です。
2. コンポーネントの結合(Coupling)
コンポーネント同士の依存関係をどのように構築するかを決める原則です。
- ADP: 依存関係に循環を作らない
- SDP: 依存は不安定なものから安定したものへ向ける
- SAP: 安定したコンポーネントは抽象で構成する
これらの原則に従うことで、依存関係の方向を制御し、変更の影響がシステム全体に広がることを防ぐことができます。
このような設計は、クリーンアーキテクチャやドメイン駆動設計などのアーキテクチャにも深く関係しており、大規模なソフトウェアを長期的に保守・進化させていくための重要な基礎原則となっています。