onemuri.space

依存関係逆転の原則(DIP)ってなに?なんで依存性逆転しないといけないのと思っている人へ

コンポーネントの結合

クリーンアーキテクチャという言葉を最近よく聞くようになってきましたが、その中で象徴的なものに依存性逆転の原則というものがありますよね。

よく聞くようになってきましたが、一体なんなの?なんのために使う必要があるの?という方のために記事を書きます。

そのために、必要な前提の話を序盤にした後に、その理由についてお話しするので少しだけお付き合いください。

非循環依存関係の原則(ADP)

大規模な開発現場に置いて、誰かの変更を追って自分のコードにも変更を加え、何週間もビルドできないような状況を打破するために生まれた

有向非循環グラフ

循環構造がなければ、特定のレイヤーの変更によって影響を受ける範囲を特定することができる

Presenter がリリース → Main/View が影響を受ける
Entities がリリース → Database/Interactions が影響を受ける

システムリリースはボトムアップで、Entities から Database/Interactions, Presenters, View, Controller, Authorizer, Main の順でリリースすれば良い(Main からリリースすると、下位層のリリースミスによる手戻りが発生する)

有向循環グラフ

有向循環になっている場合のデメリット

  • 循環している部分の開発者たちがお互いに手を取らなければならない
  • Entities をテストする際に、Interations と Authorizer を統合しなければならない(密結合してしまう)
  • ビルドしてリリースするための正しい順番も存在しなくなる

解決方法

  1. DIP(Entities と Authorizer の間に interface を挟む)
  2. Entities と Authorizer が共通に依存するクラスを新しいコンポーネントへと移動する

の 2 つがよく使われている

トップダウンの設計

トップダウンによる設計は不可能。それだけ。

安定依存の原則(SDP)

以下のようなコンポーネントが 4 つあるとすると、X は安定している

X は A,B,C の 3 つのコンポーネントに対して責務を持っていて、変更しない理由を多く持つためだ(安定度が高い)

そして、X は他のコンポーネントに依存していないので独立している

    A
    ↓
B → X
    ↑
    C

矢印の向きが全て逆になった以下のコンポーネントの場合はどうだろうか

Y は非常に不安定で、他の 3 つのコンポーネントに依存しているので従属している

    A
    ↑
B ← Y
    ↓
    C

安定度の算出

安定とか、独立とかなんとなくわかる。では、どうやって具体的に安定度を数値化すれば良いだろうか

  • ファン・イン(Fin)
  • ファン・アウト(Fout)
  • Instability(安定度, 以降 I とする): I = Fout / (Fin + Fout)

I は 0 に近づくほどに安定する

先ほどの例で考えてみよう

X の場合

  • Fin = 3
  • Fout = 0
  • I = 0 / (3 + 0) = 0
    A
    ↓
B → X
    ↑
    C

Y の場合

  • Fin = 0
  • Fout = 3
  • I = 3 / (3 + 0) = 1
    A
    ↑
B ← Y
    ↓
    C

少し極端だが、Y が不安定であることが数値化された

この結果だけを見ると、全て安定させれば良いように思えてくるがそうではない。なぜなら、全てのコンポーネントが安定していると、変更しづらいシステムになってしまう

なので、意図的に安定度低くするコンポーネントを作り、柔軟に変更がしやすい部分を残すのだ

しかし、ここでまた別の問題が出てくる。それは安定度の高いコンポーネントを安定度の低いコンポーネントに依存させてしまった時だ。

例えば以下のようなコンポーネントのような状況において、 X が Y に依存するような流れを作ったらどうなるだろうか

    A
    ↑
B ← Y
    ↓
    C

    D
    ↓
E → X
    ↑
    F

つまりこういうことだ

    A   D
    ↑   ↓
B ← Y ← X ← E
    ↓   ↑
    C   F

こうなると、意図的に安定度を下げたはずの Y は一気に変更しづらいものになってしまう

Y を変更すると、X に修正が必要になり、その結果 D, E, F にも修正が必要になってしまうからだ

安定度の差が大きいコンポーネント同士を依存させてはいけないのである

これを解決するために DIP を使えば良い

    A       D
    ↑       ↓
B ← Y → Z ← X ← E
    ↓       ↑
    C       F

X の安定度

  • Fin = 3
  • Fout = 1
  • I = 1 / (1 + 3) = 0.75

Y の安定度

  • Fin = 0
  • Fout = 4
  • I = 4 / (4 + 0) = 1

Z の安定度

  • Fin = 2
  • Fout = 0
  • I = 0 / (0 + 2) = 0

これにより、矢印の向きは安定度の高いほうへと流れていくことができる

安定度・抽象度等価の原則(SAP)

安定度と抽象度は同程度でなければならないという原則

  • 安定度が高いコンポーネントは抽象度も高く、安定度が高いことによって拡張を妨げないようにすべき(Infrastructure)
  • 安定度が低いことで抽象度を低くし、具体的なコードを変更しやすくしておくべき(View)

抽象度の算出

  • Nc: コンポーネント内のクラス数
  • Na: コンポーネント内の抽象クラスとインターフェースの総数
  • A: A = Na / Nc

A = 0 ならば具象、 A = 1 ならば抽象を表す

これによって、安定度と抽象度を算出することができるようになった

安定度と抽象度算出による影響の可視化

縦軸に抽象度をとり、横軸に安定度(不安定さを表すので 0 に近いほど安定することに注意)をとったグラフを以下のように描いた時、

苦痛ゾーン…(0,0)近辺

  • 安定度が高いのに、抽象度が低い(具象)クラスという矛盾を抱えるため、設計されたコンポーネントがここにくることは通常ない
  • Database や具象ユーティリティクラスは例外として存在する
    • 例えば、String コンポーネントは具象でありながら変動性は低いため害はなくなる

無駄ゾーン…(1,1)近辺

  • 最高に抽象されているのに依存するコンポーネントがないような状況だ(はっきり言って無駄、ゴミ、だって使わないもん)

InstabilityとAbstractlyの図

目指すべきは(0,1)(1,0)を結ぶような直線上に近づくことである

まとめ

  • 自分が修正したことによって影響を受ける範囲が大きい場合はアーキテクチャを見直した方が良いと思います(それが無理なほど大規模になった場合は…)
  • 毎回このような計算を全てのコンポーネントに対して実施するようなことは地獄なので流石にしないが、特定のコンポーネントに対して計算してみるのはあり
  • 現在の設計はこのような原則が良いとされているが、今後変わる可能性もあるのでキャッチアップは忘れずにしていきましょう

参考

Clean Architecture 達人に学ぶソフトウェアの構造と設計

自己紹介用画像

Riki Akagi

2019年からDeNAで働いています。GCP(CloudSQL・GAE・Cloud Function etc)とGoでAPI開発に勤んでいます。睡眠やエンジニアリングに関することに興味を持って過ごしているのでその情報を皆さんに共有していけたらなと思っています。

自己紹介の詳細