依存性逆転の原則の話題に関連して
Clean Architecture 読書会に参加して、依存性逆転の原則(Dependency Inversion Principle)の話題になりました。
この読書会を受けて、以下2つのブログとtwitterでのやりとりがありました。
上記2つのブログで、「モジュールの安定度」と「上下(というかレイヤ)」という点が気になりました。以下自分なりの考察を書いてみます。
安定度と依存性の逆転について
「安定」について、Clean Architecture 本では次のように言われています。
ソフトウェアを変更しづらくするには、多数のソフトウェアコンポーネントから依存されるようにすればいい。多数のコンポーネントから依存されたコンポーネントは非常に安定している。なぜなら、少し変更するだけでもほかのさまざまなコンポーネントとの調整が必要になるからだ。
さて、こんな文言が目に留まると、次のような疑問が浮かびました。
議論が循環しているというのは、 @at_grandpa さんのブログ内では「安定したモジュールに依存させるために依存関係を逆転させる」とされていたためです。この「安定」はどこから取り出すことができるのだろうか?何を「安定したモジュール」として決定できるのだろうか?
著者のBob Martin(ボブおじさん)は「安定性」について労力との相関関係で捉えていて、「変更により労力がかかるものは相対的に安定している」と分析しています。具体例で「コインと机ではひっくり返すときに机のほうが労力がかかるため安定している」というのですが、これはかなり倒錯した言い回しだと思います。普通は「机は安定しているからひっくり返すのに労力がかかる」というのではないでしょうか。このなかに「安定」の定義はどこにもなく、あるのは「ものとそれを変更するのにかかる労力」の関係です。ボブおじさんの倒錯した言い回しは、「安定」は定義しないけどそれを変更するのにかかる労力を指標にして「安定度」(「安定」ではない)を定義しよう、という提案です。ボブおじさんはソフトウェアの「安定度」をこのアナロジーから帰結しています。ソフトウェアの場合は依存の多さが変更にかかる労力になります。あるコンポーネントに依存の向きが集中すると変更にかかるコストが飛躍的に増大するため、このコストの高さをもって「このコンポーネントは安定度が高い」と表現しているとおもいます(繰り返しますがこれは常識に対して倒錯しています)。
この「安定性」の概念をとるなら、これはべつにOOPに限った話ではなく、例えばSQLは非常に安定している、なぜならSQL標準に依存したソフトウェアは山ほどあるためSQL標準仕様を変更するとなると経済に打撃を与えるレベルのコストがかかるからだ、となると思います。この分析枠組みの中では、どうやっても「依存の方向」が「安定」を決定するファクターになると思います。
ここで、依存性逆転、つまり「依存の方向性を変える」という話に戻ります。いままでの話では、「依存の方向」が「なにが安定したコンポーネントか」を決定しているという話でしたが、プログラマはこの依存の方向を変えることができる。この原則を敷衍するなら、依存の向きを変えることによって「なにが安定したコンポーネントなのか」をプログラマが決定できるということを意味すると思います。依存の方向を操作することができるということは、お望みのコンポーネントの安定度を高めることができる、つまりあるコンポーネントの「安定度」はプログラマの恣意的な操作対象である。これこそがボブおじさんが感嘆を込めて「これはパワーだ!」といっていることなのだと思います。
そうすると、ここで @at_grandpa さんの記述している「安定依存の原則」がでてくるとどうなるだろう?おそらく、実際に循環を始めることになる。安定度は依存の方向の集積として決定されるので、この安定度を再度利用して依存の方向を安定側にむけるということは、安定度がますます高くなるサイクルに入ることを意味していると思います。望もうにも全く変更が効かなくなるようなコンポーネントが生まれてくることになると思います。
では、このような全く変更が効かなくなるコンポーネントを作る理由はなんでしょうか?おそらく、それは再利用性が理由になるとおもいます。あるコンポーネントが再利用可能であるということは、このコンポーネントは他からの依存を受け付けることができるということなので、安定度は増加する一方で変更は困難になります。つまり安定度と再利用性は相関し、再利用性と変更可能性は相反します。
「上下」のメタファ
ここまでは、単に「依存性の逆転」と書いて「の原則」を書かないできました。「依存性の逆転」はプログラマが依存関係を書き換えることができるというだけで、それ以上の意味はないと思います。ところでその「原則」となるとどうやら違って、望ましい依存関係はどんなものかまで突っ込んで定義しています。@hidenorigoto さんのブログから、依存性逆転の原則のステートメントの翻訳をお借りします。
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.(上位レベルのモジュールは下位レベルのモジュールに依存すべきではない。両方とも抽象(abstractions)に依存すべきである。)
B. Abstractions should not depend on details. Details should depend on abstractions.(抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。)
Dependency inversion principle - Wikipedia
「望ましい依存関係」が定義されているのはわかりますが、「上位レベルのモジュール」「下位レベルのモジュール」「抽象」「詳細」といったよくわからない概念が出てきます。
「上位レベルのモジュール」「下位レベルのモジュール」ってなんだ?とりあえずこれについて考えるためにClean Architecture本が言及している図をみてみましょう(図5-1と呼びます)。
この図はClean Architecture本の第五章「依存関係逆転」に掲載されています。mainから始まって下に向かうように図示されていますが、たぶんこの図は下にいくに連れてレイヤを下っています(本文ではあまりに説明不足ですが)。図をよく見てみると、各コンポーネントの名前は「HL」「ML」「LL」となっており、おそらくHigh-level、Middle-level、Low-levelの略でしょう。これらのコンポーネントはレイヤリングされていて上層レイヤから呼び出されて下層レイヤにアクセスしています。また、各階層を下るにつれ、例えばHL1からはML1、ML2へ依存の矢印が伸びていて上位レイヤのコンポーネントのほうが数が少ないことも示されていそうです。
この図の処理内容をイメージするには、たとえばユーザーの入力をファイルに出力するという状況を考えると良いのではないでしょうか。プログラマはその言語のファイル書き出し関数を呼び出しすだけですが、その関数がさらにレイヤを下って「OSのAPIを呼び出す→OS側でディスクにアクセスして物理的に書き込む」などの処理になっていると思います(詳細は略しています)。これが下層レイヤに下っていくイメージだと思います。上下はあくまでメタファですが、物理層に近づくほど「低レイヤ」と呼ぶのはかなり一般化したメタファだと思います。
レイヤ
詳細な処理の流れについて普段プログラマが考えないで済むのは、OS側がアプリケーションレイヤと物理レイヤを切り分けて抽象化しているためだとおもわれます。アプリケーションが物理レイヤに対してほぼ独立して(つまり可搬性をもって)記述できるのはレイヤリングの恩恵なのではないでしょうか。
こういうOSのようなレイヤリングというアイデアを普段のプログラミングにも導入すべきだという主張はDIPの前提になっていると考えられます。
こういったレイヤリングは容易に破綻します。図5-1のように、上位レイヤから下層レイヤを直接触るということは下層レイヤへの依存を意味しています。例えばよくあるのはディレクトリへのパスをスラッシュ記法(またはバックスラッシュ記法)でかくことなどでしょう。開発マシンがWindowsで本番サーバーがLinux系のサーバーだとしたらどちらかが動きません。これはアプリケーションから見て下層レイヤの知識(この場合は具体的なファイルシステム)に、アプリケーション(上位レイヤ)が依存している単純な例です。DIP原論文ではCopyプログラムのサンプルについて考察していて、良くできた例があります(日本語ではこの記事が詳しく説明しています)。
望ましい依存関係
OSとプログラミング言語の関係を考えてみると、プログラミング言語のほうがOSに依存しています。プログラムは最終的には何かしらのハードウェアリソースにアクセスしますが、このハードウェアリソースへのアクセスは基本的にOSが制御しています。同じように、データソースとして例えばMySQLを利用しているとしたら、このデータソースはMySQLが制御しているので、プログラム側は依存することになります(MySQLが用意した手続きにプログラムが合わせなければならない)。処理の向きはプログラム→OS、プログラム→MySQLとなっているのにあわせて依存の向きも同じになっています。つまり、プログラムはプログラム外の低レイヤなものたちに依存しています。
この状況がもたらす問題点は明らかだとおもいます。特定のOSでしか動かないプログラム、特定のSQLを書いたプログラムの可搬性・再利用性の低さはプログラマであれば何度も経験しているものと思います。依存性逆転の原則の主張とは、抽象を導入することでこの低レイヤへの依存を解除することができ、プログラムが再利用できるようになる、という話のように思われます。Windowsでしか動かない・MySQLでしか動かないというのでは再利用性・可搬性が低いので、抽象を用いてレイヤリングし、アプリケーションに接続するドライバを記述させることでOSもデータストアもプラグインとすることができます。
逆の点から考えてみましょう。もしOSが個別のアプリケーション実装に依存していたらどうなるでしょうか?もちろんOSはアプリケーションから独立して更新できなくなります。OSは、アプリケーションを自らに依存させるように作られています。OSから見れば個別アプリケーションは詳細です。これはおそらく他のどういったシステムでもこのように作っていると思われます。こういった事情があるがゆえに、アプリケーションはなにかしら外部のシステムと接続する際に望ましくない(再利用できない)依存関係を作り出してしまう。幸い、プログラマは依存関係を操作できるので、この依存関係を逆転させましょう。
安定度の話に戻ってみます。あるアプリケーションがWindowsだけでなくMacやLinuxでも動くようにするというのはそうする必要があったからと考えられ、そうするとこのアプリケーションに対する依存の向きが3つ増え、逆にこのアプリケーションから出る依存の向きは減り、そのぶん安定度が増します。安定度が増したということは変更困難度もましているということですが、これはプログラミングの可塑性を失っている状態なのではないでしょうか?おそらく、まさしくそうなのだとおもいます。理屈上は、依存性逆転の原則を繰り返し使うとほとんど変更できなくなるほど依存されたコンポーネントができてくるはずです。依存性逆転の原則とは、活性層と非活性層を分離する作業であり、変化にかかるタイムスパンをコントロールする技法だとまとめても差し支えないのではないでしょうか。ぼくはへぼプログラマなのでLinuxカーネルの実装などを読んだことがないのですが、こういったいわば「枯れた」層と変化が激しい活性層があることは容易に推測できます。
おわりに
具体例も少なく、あまり整理もできていないのですが、長くなって力尽きてしまいました。レイヤに対する考察は自分でも足りなすぎると思っています。
言及させていただいた @at_grandpa さんと @hidenorigoto さんのブログとは、結論という点ではおそらく同じになっていると思います。安定度に関してもっと突っ込んだ議論ができるとおもしろいとおもってこの記事を書きました。
おもったこととしては、ボブおじさんは逆説家だなということです。おそらく Inversion という言い回しも常識に逆らう言い回しを好むのだろうと思います。安定についてのトリッキーな記述はすごく面白いとおもいます。
全く関係ないのですが、著者の「ボブおじさん(Uncle Bob)」というあだ名を聞いてぼくは絵画教室のボブを思い出しました。Clean Architecture本文中の「ね、簡単でしょ」という記述には吹きました。ぜったい意識しているとおもうんですよね。