はじめに
私はPHPの開発でRepository層を扱うことが多かった。特にLaravelでは私が知っている限りにおいてapp/repositoriesというのは一級市民として独立したレイヤーを獲得していた。
最近、Clean Architectureの文脈ではDomainがI/Fだけを持ち、呼び出される側が実装だけを持つのだということを知った。DIP(依存性の逆転)というらしい。
私にとってのI/Fの原体験はJavaのそれであり、容易に実装を変更するためにシグニチャという制約を持つ同じレイヤーに置かれたものでしかなかった。
これらの経験から違和感を覚えたものの、それをすぐには言語化できなかったので、どうしてこの様な形になるのだろうか?ということに対して自分なりの結論を出すことにした。
Repository とは何だったのか
Repository の置き場所は、流派によって違う。
- Evans(DDD): Repository をドメインに留め、分割しなかった。ドメインの構成要素の一つとして扱う立場。
- Laravel 的な実装: Repository を独立した層として切り出した。ただし、その多くは ORM のオブジェクトをそのまま返し、翻訳を素通りさせている。
- Clean Architecture: 外縁に実装を置き、内部にインターフェースを置いた。
三者は、まったく別のことをしているように見える。だが、Repository を「外部との翻訳装置」だと捉え直すと、見え方が変わる。外部の様式(SQL、JSON、HTTP のレスポンス)を、内部の語彙(ドメインの言葉)に変換する装置。そう考えれば、三者の違いは、その翻訳装置を どこに置くか の違いでしかない。
翻訳装置そのものは、どの流派にも必要だ。問題は、その装置に、翻訳以外の動機が混ぜ込まれているかどうかである。そして、Clean Architecture がその装置に混ぜ込んだ動機こそが、依存性逆転だった。
まず、逆転しているものを認める
依存性逆転を批判する前に、それが何を正しく成し遂げているかを、先に認めておきたい。ここを飛ばすと、議論が噛み合わなくなる。
依存性逆転が逆転させているのは、ソースコードの依存方向である。コンパイル時の依存、と言ってもいい。
通常、高レベルの処理(ドメイン)が低レベルの処理(インフラ)を直接呼べば、ドメインのソースがインフラのソースに依存する。依存の矢印は、ドメインからインフラへ向かう。依存性逆転は、ここに抽象(インターフェース)を挟む。ドメインはインターフェースに依存し、インフラもインターフェースを実装することで、インターフェースに依存する。結果として、ソースコード上の依存の矢印は、両側から内側(インターフェース)へ向かう。
これは、事実である。コンパイル時の依存図の上で、矢印は確かに付け替わっている。ドメインのソースは、もはやインフラのソースを名指していない。この一点において、依存性逆転は、何も嘘をついていない。ソースの依存は、確かに逆転する。
だから「依存性逆転は嘘だ」と言うと、この正しい事実によって、正当に反論される。逆転は、起きている。ただし——ソースという、一つの視点に限って。
そもそも、新しい機構は何もない
ソース依存が逆転することを認めた上で、もう一歩、手前に踏み込みたい。依存性逆転は、いったい何を発明したのか。
分解してみると、新しい部品は一つもない。間接呼び出し——ポリモーフィズム、関数ポインタ、仮想テーブル——は、何十年も前からある。インターフェースを介して抽象に依存させるのも、昔からある手法だ。依存性逆転がやったのは、その既存の手法を使ったときに「ソース依存図の矢印がこう描ける」という見方を提示し、それに「逆転」という名前を与えたことである。
つまり、依存性逆転は、技法ではない。技法の記述であり、命名である。新しい機構を発明したのではなく、ありふれた間接呼び出しに、ソース依存という特定の視点から照明を当て、「逆転」という劇的な語を被せた。
ここが、違和感の本当の源だったと思う。実行時に何も変わらないのは、変わりようがないからだ。動いている機構は、昔ながらの間接呼び出しのままで、依存性逆転によって新しい何かが加わったわけではない。変わったのは、依存図の描き方と、その呼び名だけ。機構は、一ミリも動いていない。
その裏で、何も逆転していない
このことは、実行時の流れを追えば、すぐに確かめられる。ドメインが、インターフェース越しに、インフラの機能を呼ぶ。呼ばれたインフラが、実際の処理をする。処理結果が、ドメインに返る。制御は、ドメインからインフラへ向かい、そして戻ってくる。値の流れも同じだ。ドメインが要求し、インフラが応え、値がドメインへ返る。
実行時の向きは、ドメインからインフラへ、そして戻る。ソースの依存の向き(インフラからインターフェースへ、内向き)とは、逆である。同じシステムの同じ部分が、見る視点によって、正反対の向きを持つ。コンパイル時のソース依存は内向き。実行時の制御と値の流れは外向き。実装と実行が、真逆を向いている。
ここで大事なのは、この「真逆」が、依存性逆転に固有の現象ではない、ということだ。間接呼び出しを使えば、どこでも起きる。呼び出し側が抽象を名指し、実行時には具象へ制御が流れて戻る——間接呼び出しに普遍的なずれである。だから、これを「依存性逆転に固有の欠陥を見つけた」と読むのは、的を外している。正しくは、こうだ。実装と実行が真逆になるのは、依存性逆転が、間接呼び出しそのものに過ぎないことの、証拠である。
私の違和感の正体は、ここにあった。書いた構造(ソース)と、動く実態(実行)が、真逆になる。一方を見れば他方も分かる、という素直さがない。見る層ごとに、向きを別々に追わなければならない。そしてそのずれは、新原理がもたらしたものではなく、ありふれた間接呼び出しが、もともと持っていたものだった。
「逆転」という語彙が粉飾するもの
ここで起きているのは、嘘ではなく、粉飾だと思う。
事実は二つあり、どちらも正しい。ソース依存は逆転する。実行時の流れは逆転しない。この二つは矛盾なく両立する。逆転するのは、限定された一つの軸——コンパイル時のソース依存——だけだからだ。
問題は、この限定的な事実が、「依存性逆転」という語彙で語られること。「逆転」という言葉は、何かが根本からひっくり返る、という強い印象を与える。実際にひっくり返っているのは、ソース依存という一軸の矢印だけなのに、「逆転」という語は、依存そのものが、流れそのものが、関係そのものが反転したかのような、過大な像を結ばせる。
限定的なソース依存の整理を、流れの逆転であるかのような語彙で語る。これが粉飾である。嘘ではない——ソース依存は本当に逆転している。だが、その限定的な事実に、実態を超えた大きさの語彙をかぶせている。
そして、前節を踏まえると、粉飾されているものが、もう一つ見えてくる。新規性である。
依存性逆転は、新しい機構を発明していない。ありふれた間接呼び出しに、視点と名前を与えただけだ。にもかかわらず、「逆転」という語は、依存関係を根本から組み替える新原理であるかのような響きを持つ。つまり、「逆転」という語の過大さは、機能の過大評価というより、発明の過大評価だった。ありふれた機構を、新原理のように見せる。限定的なソース整理を、流れの逆転のように見せる。粉飾は、この二重の過大評価として働いている。
ただし、ここで行き過ぎないように、反論を置いておく。「ただの命名に過ぎない」と振りかざすのは、不正確だ。命名と視点の提示には、確かな実利がある。ソース依存の矢印をどちらに向けるかを、設計者が意識的に制御の対象として扱えるようになったこと自体は、間接呼び出しを漫然と使うのとは違う達成だ。同じ機構でも、それを「依存方向の制御対象」として名指したことで、レイヤー境界やビルド単位の設計が、議論できるものになった。顕微鏡は新しい臓器を作らないが、見えなかったものを見せることには価値がある。依存性逆転は、新しい臓器を作っていない。しかし、見方を提供した。
だから、批判は、技法そのものにも、命名の実利にも、向けるべきではない。向けるべきは、その命名が「逆転」という語を選び、ありふれた機構を新原理のように、限定的なソース整理を流れの逆転のように、見せてしまったことに対してだけである。
そして、この粉飾は、おそらく誰かが意図的に仕掛けたものではない。「逆転」という語が選ばれ、その語が、結果的に過大な印象を広めた。意図的な欺きというより、語彙の選択が、誇大な受容を生んだ。罪は概念にではなく、限定を説明しない伝えられ方の側にある。
所有という、もう一つの言い換え
粉飾は、語彙だけにとどまらない。
ソース依存を逆転させるために、インターフェースをドメイン側に置く。すると、外部へのアクセスが、あたかも「ドメイン自身の処理」であるかのような体裁が生まれる。そして、その体裁は「所有」という言葉で補強される。インターフェースはドメインが所有している、だからドメインは外部に依存していない、自分の持ち物に依存しているだけだ——と。
だが、所有という言葉は、依存の深さを何も変えていない。ドメインが所有しているのはインターフェース(形)だけで、その向こうの機能(実体)は、依然として具象が提供している。ドメインは、自分が所有する形を通じて、自分が所有しない実体に、依存し続けている。所有しているのは入れ物だけで、中身は他人のものだ。
入れ物を所有しているから中身にも依存していない、というのは、論点のすり替えである。依存という事実を、所有という別の軸の話に、ずらしている。「逆転」が向きの語彙による粉飾なら、「所有」は依存の軸そのものをずらす言い換えだ。
返り値を受け取って「しまう」
実装と実行が真逆である、ということは、ひとつの具体的な現象として現れる。返り値だ。
依存性逆転を名乗る Repository は、たいてい値を返す。user = repository.findById(id) のように、インターフェースを呼んで、戻り値を受け取る。
結果を受け取っているということは、実行の流れが、呼んだ側に戻ってきている、ということだ。流れが戻ってくるなら、実行時の制御は、途切れていない。行きも帰りも、ひとつながりの線の上にある。ソースの上では内向きに整理されていても、実行の上では、連綿とつながった一本の線が、行って戻ってくる。
しかも、これは「受け取れる」のではなく「受け取って しまっている」。意図して結果を受け取る選択をしているのではない。構造が呼び出し元と呼び出し先を連綿とつないでいるから、結果が、否応なく、手元に戻ってきてしまう。実行の流れが一本につながっている限り、戻り値は、避けようもなく返ってくる。
返り値を受け取ってしまうこと。これが、実行時には何も逆転していないことの、最も分かりやすい証拠である。
本当に逆転するなら、そこで止まる
では、実行時にも本当に逆転するとは、どういうことか。
本当に逆転するなら、実行の流れは そこで止まる はずだ。呼び出した側は、結果を待たない。受け取らない。流れは、その境界で、本当に終わる。
そして、続きは、別の実行文脈——新たなプロセスやスレッド——として、改めて立ち上がる。呼び出し元が実行を続けるのではなく、別の文脈が、外から起こされて、新しく走り出す。
これなら、実行時にも依存は逆転している。呼び出し元は、続きが誰によってどう処理されるかを知らないし、結果も戻ってこない。実行の流れは、境界で本当に断ち切られている。
逆に言えば、こうした「止まる」「別の文脈として立ち上がる」がないまま、つまり結果を受け取ったまま、実行時の逆転を主張するのは、無理がある。実行時に逆転させたいなら、流れを本当に断ち切るしかない。断ち切れば、返り値は受け取れない。返り値を受け取りながら、実行時の逆転を名乗ることは、できない。
ここで、誤解のないように補っておく。結果を受け取ること自体が悪いのではない。世の処理の大半は、結果を必要とする。注文を作れば確認が要るし、データを引けばその値で次を判断する。そういう「結果が要る処理」は、そもそも実行時に逆転させる必要がない。素直に依存し、素直に取得すればいい。
逆転が問題になるのは、結果が要る処理に、実行時の逆転であるかのような衣をかぶせたときだ。ソース依存を整理しただけなのに、流れまで逆転したかのように語る。逆転そのものが悪いのではなく、ソースの逆転を、実行の逆転であるかのように語る、その粉飾が問題なのだ。
同心円は、何に似ていたか
依存性逆転を含む Clean Architecture の同心円——中心にドメインを据え、外部もインターフェースという統一された形で扱う——を眺めていると、別のものを連想する。UNIX の「すべてはファイルである」という思想だ。
デバイスも、パイプも、ソケットも、すべてをファイルという統一されたインターフェースで扱う。同心円が、外部とのやり取りをすべてインターフェースという統一形で束ねようとするのは、発想として、これに近い。
つまり、目指したもの自体は、的外れではない。統一的な抽象で外部を束ねる、という方向は、OS が長年やってきたことに通じる、まっとうな発想だ。
問題は、それを どの土台の上で実現するか にある。
OS のドライバは、Repository と同型だった
OS は、プロセスやスレッドという、本当に分離した処理を持っている。別々のアドレス空間、別々の実行文脈。互いのメモリには触れない。ハードウェアのメモリ保護が、その分離を強制している。これは、体裁ではない。本当に区切られている。
そして OS は、その本当に分離した処理を、メッセージング で束ねる。プロセス間通信、シグナル、パイプ、ソケット。ひとつのプロセスがメッセージを送り、別のプロセスが、自分の文脈で、それを受けて処理する。送った側は、相手の内部を知らないし、多くの場合、結果を待たずに進む。
これは、まさに「止まって、別の文脈として立ち上がる」だ。呼び出して返り値を待つのではなく、メッセージを送って、相手が別文脈で処理する。OS は、本物の分離を、本物のやり方で束ねている。少なくともこのメッセージングの層では、依存の切り方は、ソースと実行のどちらの視点でも向きが一致していて、実装と実行が真逆にならない。
外部装置との翻訳も、OS はちゃんと持っている。ドライバだ。キーボードも、マウスも、ディスクも、ネットワークカードも、それぞれ固有の様式を持っている。その固有の様式を、ドライバが翻訳して、OS には統一された「ファイル」という顔で見せる。すべてはファイルである、という思想は、ドライバという翻訳層によって、支えられている。
そして、注目すべきは、このドライバ層の構造だ。Linux の file_operations や struct device_driver は、関数ポインタのテーブルである。カーネルは、具象ドライバを名指さない。ドライバ側が、インターフェース(関数ポインタの集合)を実装して、登録する。ソース依存の向きで言えば、これはまさに依存性逆転そのものだ。しかも、read() は値を返す。実行時には、値が呼び出し側に戻ってくる。
つまり、OS のドライバ層は、構造的に、Clean Architecture のアダプタ層と、同型である。依存性逆転で、値を返す。先ほどまで「義足」と呼んできたものと、構造は変わらない。
これは、OS と Clean Architecture を対立させて、片方を本物、片方を偽物とする見立てが、的外れだったことを意味する。両者は、対立していない。同じことを、やっている。外部の固有の様式を、依存性逆転を使った翻訳層で受けとめ、統一された語彙に変換して、内部に提供する。OS はそれを巨大なスケールで、Clean Architecture はアプリケーションのスケールで、やっている。発想も、構造も、同じだ。
その同型を、最も鮮やかに示すのが /dev だ。ドライバが翻訳した結果は、/dev というディレクトリに、ファイルとして集約される。デバイスの固有の様式は、ドライバによって翻訳され、統一された「ファイル」という顔で、一箇所に並ぶ。外部の様式を内部の語彙に翻訳して、一元的に提供する——これは、Repository 層そのものだと言っても過言ではない。/dev は、リポジトリパターンの、完成形の一つだ。Repository とは何だったのか、という最初の問いの、一つの答えが、ここにある。
では、OS と Clean Architecture が同型なら、なぜ片方は見事に機能し、片方は不自然に見えるのか。違いは、依存性逆転そのものにはない。その背後の、土台にある。
OS のドライバの依存性逆転は、本物の分離(プロセス、メモリ保護、カーネル空間とユーザー空間の分離)という、別の土台の上に乗っている。分離は、すでに別の機構が保証している。だから、ドライバ層は、分離を演出する必要がない。身軽でいられる。翻訳だけに、専念できる。だから /dev という、美しい翻訳層になれる。
一方、Clean Architecture の依存性逆転は、背後に、本物の分離がない。それなのに、依存性逆転に、分離の役目まで期待する。同型のはずの翻訳層が、分離まで背負わされる。その重さが、ポートやアダプタという語彙の膨張を呼ぶ。同じ依存性逆転、同じ翻訳層でも、本物の分離という土台があるかないかで、/dev になるか、義足になるかが、分かれる。
問題は、依存性逆転にも、翻訳層にもなかった。それらは、OS で立派に機能している。問題は、本物の分離という土台がないまま、依存性逆転に分離を期待することにあった。
ポートとアダプタは、何の代償か
ここまで来ると、ポートやアダプタという語彙が、なぜ膨張するのかも見えてくる。
OS のドライバは、file_operations という、ごく地味な関数ポインタのテーブルで足りている。仰々しい「ポート」も「アダプタ」も、名乗らない。同じ依存性逆転による翻訳層なのに、OS のそれは身軽で、Clean Architecture のそれは語彙が膨れ上がる。この差は、背負わされているものの差だ。
本当に区切られていれば、区切りそのものが通信の仕組みを内蔵している。プロセスにはプロセス間通信があり、ドライバの下にはカーネルとユーザー空間の分離がある。区切りが本物なら、分離は土台が担うので、翻訳層は翻訳だけに専念でき、file_operations のように地味でいられる。
ポートやアダプタが、いかにも分離していそうな佇まいを必要とすること自体が、裏を返せば、そこに本物の分離がない、という告白なのかもしれない。本物の足があれば、義足は要らない。ポートとアダプタは、翻訳層が悪いのではなく、翻訳層に分離まで背負わせた、その重さの現れなのだ。/dev と同じ構造のものが、土台を欠いたために、代替手段として必要を迫られるのだ。
アーキテクチャは、思想である
依存性が逆転するのは、一つの視点から見た一面に過ぎない。そしてそれは、皆がそれを逆転だと信じ合う紳士協定によって、はじめて成立する。ただし、これは Clean Architecture に限った話ではない。Clean Architecture は技術を提供しているのではなく、思想を提供している。思想である以上、そこに紳士協定が含まれるのは当然で、それは要するに、制約を課しているということなのだ。
ここまで来て、ようやく、違和感の最も深い層に手が届く。
そもそも、なぜ依存性逆転は、視点によって逆転して見えたり見えなかったりするのか。実装者の視点から見れば、確かに逆転している。実行結果の視点から見れば、何も逆転していない。同じものが、立つ位置で、正反対に見える。
これが起きるのは、アーキテクチャが技術ではなく、思想だからだ。
アーキテクチャの大半は、何らかの規則を守る制約としての、構造の提案でしかない。「クリーンであれ」「依存を逆転させよ」「関心を分離せよ」——これらは、こうあるべきという方向と制約を示すが、それをどう実現するかは、示さない。実現方法を提示しないまま、こういうものを実現すればクリーンになるだろう、と問いかける。そして、その問いかけに対して、様々な人間が、様々な方法を模索する。これが、実態なのだと思う。
技術なら、こうはならない。間接呼び出しもメッセージキューも、どう動くかが一意に決まっている。動きが一つだから、誰がどの視点から見ても、たどり着く実態は一つに定まる。
だが思想は、制約しか示さない。実現は、各人に委ねられる。だから、同じ思想の下で、ある人の実装では逆転が本物になり、ある人の実装では名ばかりになる。実現が多様だから、実態そのものが、実装者ごとに分岐する。
ここで、二種類の「割れ」を、はっきり分けておきたい。混同すると、議論が濁る。
一つは、技術の割れ。間接呼び出しでは、一つの実態(昔ながらの間接呼び出し)が、視点によって、逆転して見えたり見えなかったりする。実態は一つで、見え方が二つに割れる。これが、依存性逆転がソースと実行で真逆に見える、あの現象だ。間接性ゆえの割れ。
もう一つは、思想の割れ。Clean Architecture という思想の下では、実装者ごとに、逆転が本物にもなり、名ばかりにもなる。見え方ではなく、実態そのものが、複数ありうる。実現を縛らないゆえの割れ。
技術は決定論的であり、思想をどう実現するかは確率論的であるとも言える。技術は、一つの実態が二つに見える。思想は、実態が複数ありうる。前者は見え方の分岐、後者は実態の分岐。質が、違う。
私が違和感を覚えたのは、この二つを混同していたからだ。依存性逆転が視点で割れる(技術の割れ、間接性ゆえ)のと、Clean Architecture が実装者ごとに本物にも名ばかりにもなる(思想の割れ、実現ゆえ)のを、同じ「割れ」として、ひとくくりにしていた。そして、思想の割れを、技術の割れのように——一つの正しい実態があるはずだと——扱おうとしていた。だから、複数ありうる実態を前に、唯一の正解を探して、混乱した。
そして、これは欠陥ではない。思想であることは、弱みであると同時に、強みでもある。実現を縛らないからこそ、特定の言語やフレームワークを超えて、語り継がれる。技術は陳腐化するが、思想は残る。問題は、思想であること自体ではない。思想を、技術であるかのように——たった一つの正しい実現があるかのように——扱うことだ。
だから、肝要なのは、それをそういうものだと受け入れることなのだと思う。アーキテクチャは思想であり、思想の下では、実態が複数ありうる。一つの正しい実態があるはずだ、と求めるから、混乱する。実態が分岐するものだと受け入れ、そのうえで、自分がいまどの実現を選び、どの視点に立っているのか——実装の話をしているのか、実行の話をしているのか——を意識する。そうすれば、実装者の視点では逆転している、実行の視点では逆転していない、そのどちらも正しい、と引き受けられる。混乱は、確率論的な思想を、決定論的な技術のように扱うときにだけ、生まれる。
おわりに
整理すると、私の違和感は、こういうことだったらしい。
Clean Architecutureの文脈では実装という一面から見れば、依存性が確かに逆転する。ただし、それはコンパイル時という、一つの視点に限る。その裏で、実行時の制御も値の流れも、何も逆転していない。実装と実行が、真逆を向く。そしてそれは、依存性逆転が新しい機構を発明したのではなく、ありふれた間接呼び出しに「逆転」という名を与えた、命名にすぎないからだ。機構が変わらない以上、実行時が変わりようもない。その限定的なソース依存の整理が、「逆転」という語彙によって、流れの逆転であるかのように、そして新原理であるかのように、粉飾される。
返り値を受け取って しまう のは、実行時には何も逆転していないからだ。本当に実行時にも逆転させたいなら、処理はそこで止まり、結果に応じて、新たなプロセスやスレッドとして立ち上がるしかない。OS が、本物に分離した処理を、メッセージングで束ねているように。
目指した方向は、間違っていなかったと思う。統一的に束ねるという発想は、OS にも通じる、まっとうなものだ。OS のドライバと /dev は、依存性逆転による翻訳層という意味で、Repository と同型でさえある。ただ、OS がその翻訳層を本物の分離という土台の上に乗せて身軽に保っているのに対し、Clean Architecture は、土台のないまま、翻訳層に分離まで背負わせた。その重さが、ポートやアダプタという語彙の膨張として顕在化している様にも見える。
繰り返すが、これは Clean Architecture を捨てろという話ではない。ソース依存を整理する技法として、依存性逆転は、確かに正しく機能する。問題は、その限定的な正しさが、限定を説明されないまま、過大な語彙で広まっていることだ。
結果が要る処理を素直に書くなら、実行時の逆転など名乗らなければいい。本当に分離が要るなら、義足ではなく、本物の分離を選べばいい。どちらを選ぶにせよ、自分がいま、ソースの視点の話をしているのか、実行の視点の話をしているのか。本物の区切りの上にいるのか、その代わりの義足の上にいるのか。それを区別できるかどうかが、分かれ目なのだと思う。
そして、その区別ができるのは、アーキテクチャを思想として扱えたときだけだ。アーキテクチャは、技術ではない。制約としての構造の提案——思想である。技術は決定論的で、一つの実態が視点によって違って見えるだけだが、思想をどう実現するかは確率論的で、実態そのものが実装者ごとに分岐する。それを、技術のように一つの正解があると求めれば、混乱する。実態は分岐するものだと受け入れ、自分がどの実現を選び、どの立ち位置にいるかを意識する。依存性逆転が、実装の視点では逆転し、実行の視点では逆転しないことに、そして Clean Architecture が、ある人の手では本物に、ある人の手では名ばかりになることに私が違和感を覚えたのは、確率論的な思想を、決定論的な技術のように扱おうとしていたからだった。
依存性逆転は、嘘ではない。ただし、ある視点に限る。その「ただし」を見落とさないこと——思想を思想として扱うこと——が、すべてだった。