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 は、ブラウザの種類・権限・テストの環境で振る舞いが変わる、いちばん変わりやすい道具なので、ポートの向こうへ置いています。ポートを挟むのは、変わりやすい道具だけです。
ここでいう安定は、「最近コミットが無かった」という意味だけではありません。多くの場所から使われていて容易に変更できない、という構造上の事情もあります。また、ブラウザAPIの名前が頻繁に変わらなくても、権限や実行環境によって結果が変われば、方針だけを試すときの難しさは残ります。変更の頻度と、テスト環境で制御したい事情を分けて見ます。
そのため、ポートを置く理由を「変わりやすいから」の一言だけにしないほうが、あとで判断しやすくなります。コピーなら、方針のテストからブラウザを外し、成功と拒否を選びたいという理由があります。外部の請求や時刻の取得などにも、異なる理由で境目が役立つことがありますが、この回では実装を増やさず、目の前のコピーの必要から選んでいます。
挟みすぎると、何が起きるか
反対に、何にでも約束を挟むと、別の困りごとが生まれます。1つ目は、読む場所が増えることです。「コピーの道具は何か」を知るのに、約束のファイル、実装のファイル、組み立て役のファイルの3つを開くことになります。差し替えや独立した検証の必要が無いなら、その行き来の費用が利点を上回る場合があります。
2つ目は、約束が道具の写しになることです。2ページ目で見たように、道具の手順をそのまま並べた約束は、名前がインターフェースになっただけで、方針のコードは相変わらず道具の都合を知っています。約束を挟む手間を払ったのに、差し替えも試しやすさも手に入りません。3つ目は、DI を使っただけで安心してしまうことです。コンストラクタで受け取る型が具体的な道具のままなら、依存の向きは何も変わっていません。引数で渡すことと、よい境界になっていることは別です。
このプラグインでも、表示を受け持つ presentation は、スクロールに合わせて共有欄を出し入れするために、ブラウザの window や document を直接使っています。これは表示の層そのものの仕事で、方針の判断を含まないので、約束を挟まずに済ませています。依存の境界のテストも、presentation に対しては navigator や fetch のような外との通信の道具だけを禁じ、表示のための window や document は許しています。どこまでを約束の向こうへ置くかは、層ごとに選んでいるのです。
表示の層がDOMを使うのは、その層の仕事を実現するためです。一方、どの共有方法を選ぶかという判断がDOMの操作に混ざると、判断だけを別の環境で試しにくくなります。同じwindowという名前でも、どの層が何のために使うかで評価が違います。禁止する語の一覧だけで、設計全体を決めるわけではありません。
約束を増やす案を見るときは、読む人がたどる経路も描いてみます。呼び出し側から契約、契約から組み立て、組み立てから実装へ進めるなら、境目を説明できます。名前だけ似た口が何段も続き、それぞれ何を隔てているか説明できないなら、間接化の費用を払う理由を見直します。DIPは、読む場所を増やすこと自体を勧める原則ではありません。
ポートを挟むかの目安
では、ある道具を直接使うか、ポートの向こうへ置くかは、どう決めればよいでしょうか。この記事では、次の3つの問いを順にたどることを提案します。
1つ目は、動く場所によって、その道具の中身が変わるかです。クリップボードは、ブラウザでは本物、テストでは存在せず、サーバーで動かすならまた別になります。2つ目は、その道具が無いと、方針のコードを試せないかです。クリップボードが無い Node では、直接呼ぶ形のユースケースは成功の場合を試せませんでした。3つ目は、方針の側が、その道具の細かい手順を知らずに済むかです。ユースケースが知りたいのは「書けたか、書けなかったか」だけで、navigator.clipboard?.writeText の確かめ方までは要りません。
3つとも「はい」なら、ポートを挟む候補です。「いいえ」ばかりなら、直接使ってかまいません。コピーの道具は3つとも「はい」で、言語の標準は3つとも「いいえ」でした。約束を挟むのは、変わりやすく、試すのに邪魔な道具だけにしました。何にでも挟むのではなく、挟む場所を選ぶこと。これが、DIP を実際の設計に使うということです。
三つの問いの最後は、「細かい手順を、方針の側が知らずに済むか」と読むと、ほかの二つと向きがそろいます。コピーの方針が必要とするのは、URLを書いて成功・失敗を受け取ることです。APIの有無を確認してどの関数を呼ぶかは、道具側へ任せられます。この二つを分けられることが、口を置く候補になる理由です。
問いが一つでも違う答えなら即座に不採用、という採点表ではありません。外の道具が一つでも、試すために切り離す必要が大きい場合はあります。反対に、交換できそうでも、今の仕事では単純な直接呼び出しで十分な場合もあります。分割前の困りごと、分割後に簡単になる確認、増える接続の手間を、一緒に比べて決めます。
DIP によくある3つの誤解
DIP を読むときに陥りやすい誤解を、3つ並べておきます。
| よくある誤解 | この記事での答え |
|---|---|
| DI(依存性の注入)を使えば、DIP になる | DI は渡し方の技法。受け取る型が使う側の決めた約束になっていて、初めて import の向きが逆になる |
| 具体的な道具には、すべてインターフェースを挟む | 言語の標準のような変わりにくい具象には直接頼ってよい。挟むのは、変わりやすく試すのに邪魔な道具だけ |
| 「逆転」は、処理の流れが逆になること | 処理(呼び出し)は今までどおり外へ進む。逆になるのは import の向き |
3つの誤解に共通しているのは、DIP を「インターフェースを作ること」や「引数で渡すこと」という書き方として読んでいることです。DIP が求めているのは、書き方ではなく、コードがどちらを知っているかという向きです。向きは、この記事でしたように、import を数えれば確かめられます。
最後に、図の矢印を読む三つの手順を残しておきます。まず、クリックしたときに実行される呼び出しを追います。次に、そのコードがimportしている型や実装を追います。最後に、引数と戻り値へ具体的な道具の型が漏れていないかを見ます。同じファイルでも、三回に分けて読むと、動作と依存と契約の意味を混ぜずに済みます。
この順番で説明できれば、「DIPを適用しました」という一文も具体化できます。「方針からBrowserClipboardの具体名を外し、ClipboardGatewayを受け取る形にした。実装を選ぶのは組み立て役で、成功と拒否を代役で試せる」。何が変わり、何を確かめられるようになったのかが、図の矢印と対応する説明になります。
手を動かして確かめる
今回の練習は、手元のコードで、方針のコードが具体的な道具を直接知っている場所を数えることです。たとえば、次のようなコマンドで、new で道具を作っている行や、fetch・localStorage のようなブラウザの機能を直接呼んでいる行を探せます。
$ grep -rnE "new |fetch\(|localStorage" src/見つかった行を、「変わりにくい道具(言語の標準など)」と「変わりやすい道具(ブラウザの機能・外の通信・保存先など)」の2つに分けます。変わりやすいほうの中から1つ選び、方針の側が「何をしてほしいか」だけを書いた約束(インターフェース)を1つ書いてみてください。約束に書くメソッドが、道具の手順の写しになっていないかも、あわせて見てみましょう。
このプラグインで同じように数えたときは、方針のコード(domain と application)から infrastructure への import は0件で、ブラウザの実装を名指ししているのは組み立て役の ShareContainer.ts だけでした。宿題:方針のコードから道具への矢印を数え、1つに約束を書いてみる。数えた結果は、このラベルからコメントに残せます。
練習の答えは、見つけたnewを全部消すことではありません。値を表すオブジェクトを作るnewと、外部の道具を選ぶnewは、同じ記法でも役割が違います。一つ選んだら、その部品が何の方針を持ち、どの外部条件から切り離したいのかを書いてみてください。その一文が書けなければ、まだ口を増やさずに調べる段階でかまいません。
約束を書けたら、先に小さな代役を作ってみます。必要な入力を受け取り、成功と失敗を再現できるでしょうか。具体的なブラウザの部品やデータベースの接続を用意しないと書けないなら、口へ道具の都合が残っているかもしれません。コードを移動する前に、この小さな試作で契約の形を確かめると、分割の目的を見失いにくくなります。
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サイトの制作に加えて、共有や計測のような「裏側のしくみ」の設計・導入、それらを運用できる人材の育成に取り組んでいます。テストしやすく、差し替えやすい作りをチームでそろえたい方も、お気軽にご相談ください。
記事で読んだ差し込み口と組み立て役を、実際のリポジトリで確かめるための資料です。









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