【概要】
「Clean Architecture 達人に学ぶソフトウェアの構造と設計」 は下記の通りの構成となっています。
第I部 イントロダクション
第II部 構成要素から始めよ:プログラミングパラダイム
第III部 設計の原則
第IV部 コンポーネントの原則
第V部アーキテクチャ
第VI部 詳細
この記事では「第IV部 コンポーネントの原則」の中から更に「第14章 コンポーネントの結合性」をピックアップして内容をまとめます。なお「第13章 コンポーネントの凝集性」に関しては下記の記事にてまとめましたので、そちらも参照していただけると幸いです。
また私はアーキテクチャに対する知識が浅い人間のため間違いがあればスポンジ製のマサカリを投げていただけると幸いです。
【対象読者】
- CleanArchitectureを読破済みで内容を再確認したい方
- これからCleanArchitectureを読む方で内容のあらましを知りたい方
- SOLID原則などクラス単体の設計原則を理解している方
【本題】
CleanArchitectureの「第12章 コンポーネント」にて、
コンポーネントとは、デプロイの単位のことである。システムの一部としてデプロイできる、最小限のまとまりを指す。Javaならjarファイル、Rubyならgemファイル、.NETならDLLなどがそれにあたる。
と述べられています。
またコトバンクによると結合とは、
二つ以上のものが結びついて一つになること。また、結び合わせて一つにすること
とあります。従ってコンポーネントの結合性とは関連する、あるいは関連しないコンポーネント同士の結合に関する性質かと思います。
本章ではコンポーネントの結合性に関する3つの原則が取り扱われています。
非循環依存関係の原則(ADP)
コンポーネントの依存グラフに循環依存があってはならない
あるコンポーネントに変更を加えると、そのコンポーネントに依存するコンポーネントは影響を受けます。互いに影響を与え合うコンポーネント同士は密に結合してしまっており、それらが1つの巨大なコンポーネントとなり変更を加えるのが難しくなります。この現象を回避するためにコンポーネントの依存グラフに循環依存があるべきではないという原則です。
例として下記の依存グラフにて説明します。ノードがコンポーネントを、有効エッジが依存関係を表しています。
例えばPresentersが変更された場合、影響を受けるのはViewとMainです。これはPresentersノードを起点に有効エッジを逆向きに辿ることで分かります。この依存グラフには循環依存が存在していないため前述したような巨大なコンポーネントが出来るといったことはありません。しかし要件が加わり、Entitiesに含まれるクラスがAuthorizerにあるクラスに依存してしまったとします。
この変更によりEntities, Authorizer, Interactionsの3つが循環依存となってしまいました。
例えばDatabaseに変更を加える際、循環依存が出来る前ではEntitiesとの互換性だけを考慮するだけで済んでいましたが、循環依存が出来てからはEntities, Authorizer, Interactionsを考慮しなければいけなくなります。
循環依存を排除するためアプローチとして2つ挙げられます。
DIPによる解決
上記の図はEntitiesとAuthorizerの関係を詳細に記述したもので、外側の四角がコンポーネント、内側の四角がクラスを表しています。UserクラスがPermissionsクラスに要求するメソッドを持つインターフェースを作ることで、EntitiesとAuthorizerクラスの依存関係を逆転させることができます。従って循環依存を排除することが出来ました。
新しいコンポーネントに抽出することによる解決
上記の図はEntitiesとAuthorizerの両方が依存するクラスを抽出し、新しくPermissionsコンポーネントを作ることで循環依存が排除されます。
個人的にはPermissionsコンポーネントの凝集性に問題なければ新しくコンポーネントを作り、問題があるのであればDIPによる解決を図るべきなのかと思います。
安定依存の原則(SDP)
安定度の高い方向に依存すべきである
ここでいう「安定度の高い」とは、変更がしづらいということを指します。多数のコンポーネントから依存されたコンポーネントは(変更することの影響が大きいので)変更しづらいです。安定度の高いコンポーネントHが安定度の低い(はずの)コンポーネントLに依存してしまうとコンポーネントHはコンポーネントLの変更の影響を受けてしまうためコンポーネントLは変更がしづらくなってしまいます。これを回避するためにコンポーネントは安定度が高い方向に依存すべきであるという原則です。
コンポーネントの安定度を計測する手法として、ファン・イン、ファン・アウトから求める手法があります。定義は下記の通りです。
- ファン・イン
- コンポーネント内のクラスに依存している外部コンポーネントのクラス数
- ファン・アウト
- コンポーネント内のクラスが依存している外部コンポーネントのクラス数
- 不安定さ(I)
- I = ファンアウト/(ファンイン+ファンアウト)
- I=0が最も安定度が高く、I=1が安定度が最も低いと考えます
例えば下記の依存グラフで考えてみます。Ca, Cb, Cc, Cdはコンポーネントを、q, r, s, t, u, vはクラスを、有効エッジが依存関係を表しています。
各コンポーネントの不安定さIを算出すると下記のようになります。
- CaのI = 0
- CbのI = 0
- CcのI = 1/4
- CdのI = 1
この依存グラフが安定依存の原則を遵守しているかを確認します。
それぞれの依存関係を文章化すると下記の通りになります。
- Ca(I=0)はCc(I=1/4)に依存している
- Cb(I=0)はCc(I=1/4)に依存している
- Cc(I=1/4)はCd(I=1)に依存している
- Cd(I=1)はどこにも依存していない
いずれも安定度が低いコンポーネントが安定度が高いコンポーネントに依存していることが分かります。従ってこの依存グラフは安定依存の原則を遵守していると言えます。
安定度・抽象度等価の原則(SAP)
コンポーネントの抽象度は、その安定度と同程度でなければならない
安定度の高いコンポーネントは抽象度も高くあるべきで、安定度の高さが拡張の妨げになってはいけません。一方で安定度が低いことによってその内部の具体的なコードが変更しやすくなるため、安定度の低いコンポーネントはより具体的であるべきであるという原則です。コンポーネントの抽象度Aを表す指標は以下のように求めることができます。
- Nc:コンポーネント内のクラスの総数
- Na:コンポーネント内の抽象クラスとインターフェースの総数
- A:抽象度。A = Na / Nc A=1が最も抽象度が高く、A=0が最も抽象度が低いと考えます。
下記のグラフは不安定さIと抽象度Aの関係を表したのです。(0, 1)から(1, 0)に伸びる斜線が抽象度と安定度が一致している理想を表しています。
ここで安定度が高く抽象度が低い苦痛ゾーン、安定度が低く抽象度が高い無駄ゾーンが定義されます。
- 苦痛ゾーン(安定度:高 抽象度:低)
- 抽象度が低いにも関わらず多くのコンポーネントから依存されているため、多少の変更でも大きな苦痛を伴います。
- データベーススキーマやStringコンポーネントが該当します。
- ただしStringコンポーネントは変動性が低い(変更されにくい)です。変更に苦痛が伴うのが問題なので、こういった変動性が低いコンポーネントは苦痛ゾーンに存在しても問題ないです。
- 無駄ゾーン(安定度:低 抽象度:高)
- 抽象度が高いにも関わらず、それに依存するコンポーネントが存在しません。
- 例えば実装が一つもないまま放置された抽象クラスや、そもそも使われていないクラスなどが該当します。
- 当たり前ですが無駄な要素がある状態は好ましくないです。
【まとめ】
- 非循環依存関係の原則
- 「コンポーネントの依存グラフに循環依存があってはならない」という原則
- 安定依存の原則
- 「安定度の高い方向に依存すること」という原則
- 安定度・抽象度等価の原則
- 「コンポーネントの抽象度は、その安定度と同程度でなければならない」という原則
私が本章を読んで受けた印象としては循環依存はともかく、コンポーネントの安定度や抽象度が計測・管理されているサービスは果たしてこの世にどれだけあるのだろうか?というものです。SDPおよびASPは実用的というより学術的なもののように感じます。もしこれらの数値を計測してくれるツールをご存知でしたらコメントにて共有いただけると幸いです。
また概要にも書かせていただきましたが私の考えに誤りがある場合はコメントにてスポンジ製のマサカリを投げていただけると幸いです。