DIP――ブラウザの機能を、ポートの向こうへ置く
作成: / 公開: / 内容更新:
執筆:tamito0201 / 掲載・運営:プロマリ
丸い共有ボタンの裏側を掘る連載の第8回。SOLID の5つ目の原則 DIP を、「コピー」のボタン1つから確かめます。コピーを頼むコードはブラウザを知らず、約束(ポート)だけを知る。実行時の呼び出しは外へ、import は内へ。import の向きを数え、DI・DI コンテナ・ファクトリの役目を読み、わざと境界を破って何が止めるかまで、図16点で読み解…
何にでもポートを挟まない
ここまで、コピーの道具をポートの向こうへ置くと、テストで代役を差し込め、ブラウザの事情が1つのファイルに閉じ込められることを見てきました。では、コードの中の具体的な道具には、すべて約束を挟むべきなのでしょうか。答えは「いいえ」です。このページでは、DIP の限界と、使いどころを考えます。
変わりにくい具象には、頼ってよい
実は、方針のコードも、具体的な道具にたくさん頼っています。ClipboardGateway の write が返す Promise、URL を表す string、ボタンの並びを表す Array。どれも JavaScript の言語に最初から用意された、具体的な部品です。けれど、これらにまで「文字列を扱う約束」を挟む人はいません。内側の型検査も、ブラウザの型は外していますが、言語の標準(ES2022)の型はそのまま使えるようにしています。
挟まない理由は、変わりにくいからです。Martin は、DIP のコラムと同じ連載の「Stability(安定)」の回で、安定依存の原則を示しました。原文は「THE DEPENDENCIES BETWEEN PACKAGES IN A DESIGN SHOULD BE IN THE DIRECTION OF THE STABILITY OF THE PACKAGES.(設計の中のパッケージどうしの依存は、安定している方向へ向けるべきだ)」です。変わりやすいものに、変わりにくいものを頼らせてはいけない。裏返せば、自分より変わりにくいものに頼るのは、安心してよいということです。
この物差しで、このプラグインの道具を並べてみます。Promise や string のような言語の標準は、いちばん変わりにくいので、どの層からも直接使います。DI コンテナの InversifyJS は外部のライブラリで、版が上がれば書き方が変わるかもしれないので、組み立て役の中だけに閉じ込めています。そして navigator.clipboard は、ブラウザの種類・権限・テストの環境で振る舞いが変わる、いちばん変わりやすい道具なので、ポートの向こうへ置いています。ポートを挟むのは、変わりやすい道具だけです。
挟みすぎると、何が起きるか
反対に、何にでも約束を挟むと、別の困りごとが生まれます。1つ目は、読む場所が増えることです。「コピーの道具は何か」を知るのに、約束のファイル、実装のファイル、組み立て役のファイルの3つを開くことになります。1つの約束に1つの実装しかなく、差し替える予定も無いなら、その行き来は手間だけが残ります。
2つ目は、約束が道具の写しになることです。2ページ目で見たように、道具の手順をそのまま並べた約束は、名前がインターフェースになっただけで、方針のコードは相変わらず道具の都合を知っています。約束を挟む手間を払ったのに、差し替えも試しやすさも手に入りません。3つ目は、DI を使っただけで安心してしまうことです。コンストラクタで受け取る型が具体的な道具のままなら、依存の向きは何も変わっていません。引数で渡すことと、よい境界になっていることは別です。
このプラグインでも、表示を受け持つ presentation は、スクロールに合わせて共有欄を出し入れするために、ブラウザの window や document を直接使っています。これは表示の層そのものの仕事で、方針の判断を含まないので、約束を挟まずに済ませています。依存の境界のテストも、presentation に対しては navigator や fetch のような外との通信の道具だけを禁じ、表示のための window や document は許しています。どこまでを約束の向こうへ置くかは、層ごとに選んでいるのです。
ポートを挟むかの目安
では、ある道具を直接使うか、ポートの向こうへ置くかは、どう決めればよいでしょうか。この記事では、次の3つの問いを順にたどることを提案します。
1つ目は、動く場所によって、その道具の中身が変わるかです。クリップボードは、ブラウザでは本物、テストでは存在せず、サーバーで動かすならまた別になります。2つ目は、その道具が無いと、方針のコードを試せないかです。クリップボードが無い Node では、直接呼ぶ形のユースケースは成功の場合を試せませんでした。3つ目は、方針の側が、その道具の細かい手順まで知る必要があるかです。ユースケースが知りたいのは「書けたか、書けなかったか」だけで、navigator.clipboard?.writeText の確かめ方までは要りません。
3つとも「はい」なら、ポートを挟む候補です。「いいえ」ばかりなら、直接使ってかまいません。コピーの道具は3つとも「はい」で、言語の標準は3つとも「いいえ」でした。約束を挟むのは、変わりやすく、試すのに邪魔な道具だけにしました。何にでも挟むのではなく、挟む場所を選ぶこと。これが、DIP を実際の設計に使うということです。
DIP によくある3つの誤解
DIP を読むときに陥りやすい誤解を、3つ並べておきます。
| よくある誤解 | この記事での答え |
|---|---|
| DI(依存性の注入)を使えば、DIP になる | DI は渡し方の技法。受け取る型が使う側の決めた約束になっていて、初めて import の向きが逆になる |
| 具体的な道具には、すべてインターフェースを挟む | 言語の標準のような変わりにくい具象には直接頼ってよい。挟むのは、変わりやすく試すのに邪魔な道具だけ |
| 「逆転」は、処理の流れが逆になること | 処理(呼び出し)は今までどおり外へ進む。逆になるのは import の向き |
3つの誤解に共通しているのは、DIP を「インターフェースを作ること」や「引数で渡すこと」という書き方として読んでいることです。DIP が求めているのは、書き方ではなく、コードがどちらを知っているかという向きです。向きは、この記事でしたように、import を数えれば確かめられます。
手を動かして確かめる
今回の練習は、手元のコードで、方針のコードが具体的な道具を直接知っている場所を数えることです。たとえば、次のようなコマンドで、new で道具を作っている行や、fetch・localStorage のようなブラウザの機能を直接呼んでいる行を探せます。
$ grep -rnE "new |fetch\(|localStorage" src/見つかった行を、「変わりにくい道具(言語の標準など)」と「変わりやすい道具(ブラウザの機能・外の通信・保存先など)」の2つに分けます。変わりやすいほうの中から1つ選び、方針の側が「何をしてほしいか」だけを書いた約束(インターフェース)を1つ書いてみてください。約束に書くメソッドが、道具の手順の写しになっていないかも、あわせて見てみましょう。
このプラグインで同じように数えたときは、方針のコード(domain と application)から infrastructure への import は0件で、ブラウザの実装を名指ししているのは組み立て役の ShareContainer.ts だけでした。宿題:方針のコードから道具への矢印を数え、1つに約束を書いてみる。数えた結果は、このラベルからコメントに残せます。
DIP の原典は、Robert C. Martin のThe Dependency Inversion Principle(C++ Report、1996年6月)です。安定依存の原則は、同じ連載のStabilityの回で読めます(どちらも Web Archive に残る写し)。ポートという呼び名は、Alistair Cockburn のHexagonal Architecture(Ports and Adapters)から、DI という呼び名は、Martin Fowler のInversion of Control Containers and the Dependency Injection pattern(2004)から借りています。組み立て役(Composition Root)は、Mark Seemann のComposition Rootが詳しいです。
まとめと、次回
第8回のまとめです。DIP は、方針のコードも、道具のコードも、あいだに置いた約束のほうを向く、という原則でした。コピーのボタンでいえば、コピーを頼むユースケースは ClipboardGateway という約束だけを知り、ブラウザの実装も、テストの代役も、その約束に合わせて作られています。実行時の呼び出しは外へ進み、import は内へ向かう。この向きのずれが「逆転」でした。
| 問い | 答え | 確かめ方 |
|---|---|---|
| 方針のコードは、ブラウザを知っているか | 知らない。約束(ポート)だけを知る | domain・application から infrastructure への import 0件 |
| 具体的な道具は、どこで選ぶか | 組み立て役(ShareContainer)の1か所 | BrowserClipboard の名指しは ShareContainer.ts だけ |
| 境界が崩れたら、誰が気づくか | 内側の型検査・依存の境界のテスト・代役を渡すテスト | 実験1で4件、実験2で5件が落ちた(63件中) |
| 何にでも約束を挟むか | 挟まない。変わりやすく試すのに邪魔な道具だけ | 3つの問いを順にたどる |
約束は使う側が書き、道具のほうが約束に合わせる。そして、約束を挟むのは、変わりやすい道具だけにします。前回の ISP で約束を小さく分けたから、DIP の差し込み口に、代役でも本物でも気軽に差し替えられる。第2章で読んできた5つの原則は、どれも「変わるものと変わらないものの境目に、どう線を引くか」を、別々の角度から言っていたのだと思います。
ここで大事なのは、今回の実験で加えた変更を、プラグインの本体には入れていないことです。境界を破った2つの書き換えは、見張りが止めることを確かめるためだけのもので、一時作業コピーごと片付けました。
次回は連載2.6「共有先の登録――明示登録から自動探索へ」です。今回は、組み立て役が「どの約束に、どの実装を結ぶか」を1か所で選んでいることを見ました。次回は、その選び方のもう1つの面、共有先の名札カードを、手で一覧に書いて登録するのか、置いておけば自動で見つけてもらうのか、を考えます。一緒に見ていきましょう。それでは、次回もお楽しみに!
連載の全体像は本編の連載目次へ。前回の2.4「ISP――コピーの代役に、いいねの保存まで求めない」、前々回の2.3「LSP――Copyに「開くURL」を要求すると何が壊れるか」とあわせて読むと、コピーの差し込み口が、どんな原則の上に立っているかが一続きで見えてきます。
プロマリでは、Webサイトの制作に加えて、共有や計測のような「裏側のしくみ」の設計・導入、それらを運用できる人材の育成に取り組んでいます。テストしやすく、差し替えやすい作りをチームでそろえたい方も、お気軽にご相談ください。
記事で読んだ差し込み口と組み立て役を、実際のリポジトリで確かめるための資料です。









この記事全体の感想・質問・設計へのコメント