PROMARI JOURNAL

DIP――ブラウザの機能を、ポートの向こうへ置く

作成: / 公開: / 内容更新:

執筆:tamito0201 / 掲載・運営:プロマリ

丸い共有ボタンの裏側を掘る連載の第8回。SOLID の5つ目の原則 DIP を、「コピー」のボタン1つから確かめます。コピーを頼むコードはブラウザを知らず、約束(ポート)だけを知る。実行時の呼び出しは外へ、import は内へ。import の向きを数え、DI・DI コンテナ・ファクトリの役目を読み、わざと境界を破って何が止めるかまで、図16点で読み解…

PASS IT ON

ひとつの発見を、次の会話へ。

何にでもポートを挟まない

ここまで、コピーの道具をポートの向こうへ置くと、テストで代役を差し込め、ブラウザの事情が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.(設計の中のパッケージどうしの依存は、安定している方向へ向けるべきだ)」です。変わりやすいものに、変わりにくいものを頼らせてはいけない。裏返せば、自分より変わりにくいものに頼るのは、安心してよいということです。

図6.1-1 変わりにくい具象には、頼ってよい
図6.1-1 変わりにくい具象には、頼ってよい。考え方(Martin の安定依存の原則から)。言語の標準のように変わりにくい具象は、そのまま使ってかまいません。ポートを挟むのは、変わりやすい道具だけです。図6.1-1 変わりにくい具象には、頼ってよい。考え方(Martin の安定依存の原則から)。言語の標準のように変わりにくい具象は、そのまま使ってかまいません。ポートを挟むのは、変わりやすい道具だけです。

この物差しで、このプラグインの道具を並べてみます。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つの問いを順にたどることを提案します。

図6.3-1 ポートを挟むかの目安
図6.3-1 ポートを挟むかの目安。判断の目安(提案)。変わりやすく、試すのに邪魔で、細部を知る必要のない道具だけを、ポートの向こうへ置きます。図6.3-1 ポートを挟むかの目安。判断の目安(提案)。変わりやすく、試すのに邪魔で、細部を知る必要のない道具だけを、ポートの向こうへ置きます。

1つ目は、動く場所によって、その道具の中身が変わるかです。クリップボードは、ブラウザでは本物、テストでは存在せず、サーバーで動かすならまた別になります。2つ目は、その道具が無いと、方針のコードを試せないかです。クリップボードが無い Node では、直接呼ぶ形のユースケースは成功の場合を試せませんでした。3つ目は、方針の側が、その道具の細かい手順を知らずに済むかです。ユースケースが知りたいのは「書けたか、書けなかったか」だけで、navigator.clipboard?.writeText の確かめ方までは要りません。

3つとも「はい」なら、ポートを挟む候補です。「いいえ」ばかりなら、直接使ってかまいません。コピーの道具は3つとも「はい」で、言語の標準は3つとも「いいえ」でした。約束を挟むのは、変わりやすく、試すのに邪魔な道具だけにしました。何にでも挟むのではなく、挟む場所を選ぶこと。これが、DIP を実際の設計に使うということです。

三つの問いの最後は、「細かい手順を、方針の側が知らずに済むか」と読むと、ほかの二つと向きがそろいます。コピーの方針が必要とするのは、URLを書いて成功・失敗を受け取ることです。APIの有無を確認してどの関数を呼ぶかは、道具側へ任せられます。この二つを分けられることが、口を置く候補になる理由です。

問いが一つでも違う答えなら即座に不採用、という採点表ではありません。外の道具が一つでも、試すために切り離す必要が大きい場合はあります。反対に、交換できそうでも、今の仕事では単純な直接呼び出しで十分な場合もあります。分割前の困りごと、分割後に簡単になる確認、増える接続の手間を、一緒に比べて決めます。

DIP によくある3つの誤解

DIP を読むときに陥りやすい誤解を、3つ並べておきます。

DIP によくある誤解と、この記事での答え
よくある誤解この記事での答え
DI(依存性の注入)を使えば、DIP になるDI は渡し方の技法。受け取る型が使う側の決めた約束になっていて、初めて import の向きが逆になる
具体的な道具には、すべてインターフェースを挟む言語の標準のような変わりにくい具象には直接頼ってよい。挟むのは、変わりやすく試すのに邪魔な道具だけ
「逆転」は、処理の流れが逆になること処理(呼び出し)は今までどおり外へ進む。逆になるのは import の向き

3つの誤解に共通しているのは、DIP を「インターフェースを作ること」や「引数で渡すこと」という書き方として読んでいることです。DIP が求めているのは、書き方ではなく、コードがどちらを知っているかという向きです。向きは、この記事でしたように、import を数えれば確かめられます。

最後に、図の矢印を読む三つの手順を残しておきます。まず、クリックしたときに実行される呼び出しを追います。次に、そのコードがimportしている型や実装を追います。最後に、引数と戻り値へ具体的な道具の型が漏れていないかを見ます。同じファイルでも、三回に分けて読むと、動作と依存と契約の意味を混ぜずに済みます。

この順番で説明できれば、「DIPを適用しました」という一文も具体化できます。「方針からBrowserClipboardの具体名を外し、ClipboardGatewayを受け取る形にした。実装を選ぶのは組み立て役で、成功と拒否を代役で試せる」。何が変わり、何を確かめられるようになったのかが、図の矢印と対応する説明になります。

手を動かして確かめる

今回の練習は、手元のコードで、方針のコードが具体的な道具を直接知っている場所を数えることです。たとえば、次のようなコマンドで、new で道具を作っている行や、fetch・localStorage のようなブラウザの機能を直接呼んでいる行を探せます。

Shell方針のコードが道具を直接知っている行を探す(探す語と場所は、手元に合わせて変える)
$ grep -rnE "new |fetch\(|localStorage" src/
図8-1 この回の宿題:方針から道具への矢印を数える
図8-1 この回の宿題:方針から道具への矢印を数える。確かめ方の提案。変わりやすい道具を直接知っている行が、ポートを挟む候補です。まずは1つだけ、約束を書いてみます。図8-1 この回の宿題:方針から道具への矢印を数える。確かめ方の提案。変わりやすい道具を直接知っている行が、ポートを挟む候補です。まずは1つだけ、約束を書いてみます。

見つかった行を、「変わりにくい道具(言語の標準など)」と「変わりやすい道具(ブラウザの機能・外の通信・保存先など)」の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サイトの制作に加えて、共有や計測のような「裏側のしくみ」の設計・導入、それらを運用できる人材の育成に取り組んでいます。テストしやすく、差し替えやすい作りをチームでそろえたい方も、お気軽にご相談ください。

Web制作・計測・人材育成のご相談はこちら

COMMENTS
コメント…

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

PASS IT ON

この気づきを、誰かにも。

この記事を書いた人

Takaomi Murasaki

Promari SNS Shareの開発を通して、Webの仕組みや設計の選び方を紹介しています。動く実装と、その判断に至るまでを記事に残しています。

公開記事 11 件

プロフィールを見る
THANK YOU FOR READING.すべての記事へ ↗

FROM INSIGHT TO IMPACT

「できたらいいな」を、
動く仕組みに。

記事で見つけたヒントを、あなたの事業へ。
新しいサービスも、手間のかかる業務も。
いまの課題から、つくるべきものを一緒に考えます。

開発・AI・研修の実績を見る

まだ、仕様書はいりません。

「何から始める?」から、ご一緒に。

課題がまとまっていなくても大丈夫。テーマを選ぶと相談文をご用意します。
連絡先の必須入力は、お名前とメールアドレスだけ。

まずは課題の整理から相談する

ご相談後の流れ

  1. 01 内容を確認
  2. 02 メールでご連絡
  3. 03 課題・進め方をご相談
送信だけで契約やお申込みが確定することはありません。