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

こんにちは、プロマリの紫です。連載「丸いボタンの裏側」の第8回、第2章の5回目です。SOLID は、変更に強いコードを書くための5つの原則を、頭文字でまとめた呼び名でした。第2章では、その5つを1つずつ、この共有ボタンのコードに当てて確かめてきました。今回は最後の原則、DIPです。テーマはコピーを頼むコードは、ブラウザのことをどこまで知っていればよいかということ。「コピー」のボタン1つを入口に、コードの矢印の向きを数え、わざと境界を破って何が止めるのかまで確かめます。
点線の付いた用語は、その言葉を押すと詳しい説明が開きます。意味、身近なたとえ、この実装での使い方を順に読めます。キーボードではTabで用語へ移動し、EnterまたはSpaceで開き、Escapeで本文へ戻れます。各ページで最初に出る用語から参照できるようにしました。
この記事のコードと図の土台は、promari-sns-share 4.0.0の公開ソースです。既存の実験の件数・差分は執筆時の記録として残し、今回加えた場面は「説明例」「練習例」と区別しています。現在の4.2.2にはComposeや下書きの対応もあるため、ここに載せた操作の種類や行数を、現在版の一覧としては読まないでください。各原則を、同じ実装を土台にして一つずつ確かめるための版の固定です。
この記事は連載「丸いボタンの裏側」の第8回(2.5)です。前回の2.4「ISP――コピーの代役に、いいねの保存まで求めない」で、能力ごとに小さく分けたインターフェースを先に読むと、今回の話がつながりやすくなります。連載全体の地図は、本編5ページ目の連載の目次からどうぞ。
この回は6ページあります。1ページ目で DIP の意味と、このプラグインの層の地図を確かめます。2ページ目では、プラグインを離れて、ボタンとデータベースの短い例で「逆転」の正体をつかみます。3ページ目でプラグインの実物に戻り、コピーの差し込み口と import の向きを数え、4ページ目で、実装を選んで渡す組み立て役(DI・DI コンテナ・ファクトリ)を読みます。5ページ目ではわざと境界を破って、何が止めるかを実測し、6ページ目で、何にでも差し込み口を挟まないための目安と宿題に進みます。図は20点です。既存の件数や出力は執筆時に手元で測った記録で、追加の模式図は、境界の読み方を示す説明例です。
コピーを頼む側は、ブラウザを知らない
共有欄には、SNS のボタンと並んで「URLをコピー」というボタンがあります。押すと、いま読んでいる記事の URL がクリップボードに入り、設定の例どおりなら「URLをコピーしました」と小さな知らせが出ます。やっていることは、それだけです。
けれど、このボタンの裏では、ブラウザに用意されたnavigator.clipboardという機能を呼んでいます。ブラウザによっては使えないことがあり、ページの状態によっては書き込みを断られることもあります。テストを動かす Node には、そもそもクリップボードがありません。コピーという単純な操作ほど、外の事情に振り回されやすいのです。
そこで、このプラグインでは、コピーを頼むコードに、ブラウザの機能を直接呼ばせていません。代わりに、「文字列を書き込み、成功か失敗を返す」という約束だけを用意し、それを通して頼みます。約束の名前は ClipboardGateway です。身近なもので言えば、壁のコンセントです。家電は、壁の向こうの発電所や配線を知りません。知っているのは、コンセントの形だけです。発電所のほうも、その形に合わせて電気を届けます。この連載では、この差し込み口にあたる約束を、ポートと呼びます。
このコンセントのたとえが、この回の中心となるたとえです。コピーを頼むコード(家電)は、ポート(コンセント)の形だけを知っています。ブラウザ用の実装も、テスト用の代役も、壁の向こうで、そのポートの形に合わせて作られています。だから、テストのときは代役を差し込むだけで、コピーを頼むコードを1行も変えずに動かせます。
壁の図では、まずこちら側の家電だけを見てください。家電が頼みたいのは、必要な電気を受け取ることです。発電所の機械を選ぶことや、壁の中の配線を敷くことではありません。共有ボタンでも、クリックの手順を決めるコードと、ブラウザの機能を呼ぶコードは、知っておきたい事情が違います。
ただし、コンセントの形だけ合えば何でもよいわけではありません。前々回のLSPで見たように、流れてくるものも約束を守る必要があります。コピーなら、成功したと返す前に書き込みを終え、失敗したならそれを伝えます。DIPは、その約束を誰が知る形にするかを見る原則です。交換した中身が正しく動くかは、別に確かめます。
壁を置くと、ブラウザを使わずにクリックの判断を試せます。一方で、壁の向こうのBrowserClipboardが実際のブラウザで動くかを調べる仕事は残ります。すべてのテストからブラウザを追い出すのではなく、ブラウザが要る確認と、要らない確認を分けやすくする効果だと読むと、差し込み口の目的がはっきりします。
Martin が書いた、2つの文
DIP を言葉にしたのは、SOLID の原則を広めた人として知られる Robert C. Martin です。1996年6月、C++ Report という雑誌の連載コラムで、次の2つの文にまとめました。
| 原文 | この記事での言い換え |
|---|---|
| A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS. | 大事な判断をするコード(上位)は、具体的な道具(下位)に頼らない。どちらも、あいだに置いた約束に頼る |
| B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS. | 約束が道具の細かい都合に合わせるのではなく、道具のほうが約束に合わせる |
ここで言う「上位」は、偉いという意味ではありません。コピーを頼む、共有の手順を決める、といったアプリケーションの方針を持つコードのことです。「下位」は、その方針を実際に動かす細かい道具、たとえばブラウザのクリップボードの機能です。そして「抽象」は、何をするかだけを決めて、どうやるかを決めていない部品を指します。「依存」は、あるコードが別のコードの名前を知っていて、それが無いと動かない関係のことです。
2つの文をまとめると、こうなります。方針のコードも、道具のコードも、あいだに置いた約束のほうを向く。方針のコードが道具を直接知る形をやめ、両方が約束を知る形にそろえる。これが DIP の中身です。
二つの文のうち、後半を忘れると、インターフェースを一枚作っただけで終わりやすくなります。たとえば、その口がブラウザ専用のオブジェクトを引数に求めたら、使う側は結局、そのオブジェクトの作り方を知る必要があります。間に置いた紙の名前だけでなく、紙に書いた項目が、どちらの都合で決まっているかを見ます。
「上位」と「下位」も、フォルダの上下を見て機械的に決める言葉ではありません。この例では、コピーが成功したらどんな結果を返すかは共有の手順で、どのブラウザAPIを呼ぶかは道具の詳細です。その役割を決めてから矢印を引きます。図の箱を先に作って、既存のファイルを無理にどれかへ押し込む順番ではありません。
原典の例も「コピー」だった
面白いことに、Martin がこのコラムで最初に使った例も「コピー」でした。キーボードから打った文字を、プリンタへ書き出すプログラムです。Copy という関数が、ReadKeyboard でキーボードから1文字読み、WritePrinter でプリンタへ書く。とても素直な作りです。
困るのは、書き出す先をディスクのファイルにも広げたくなったときです。Copy の中に「プリンタならこちら、ディスクならこちら」という if 文を足すことになり、出力先が増えるたびに、Copy は細かい道具を次々に知っていきます。Martin は、この Copy こそが「文字を読み取って書き出す」という、いちばん再利用したい方針を持っているのに、具体的な機器に縛られて使い回せない、と指摘しました。そして、Reader(読むもの)と Writer(書くもの)という抽象を挟み、Copy はその2つだけを知る形に直して見せました。
このプラグインのコピーのボタンも、同じ形をしています。コピーを頼むコード(方針)は、ClipboardGateway(書くもの)という抽象だけを知り、ブラウザのクリップボード(機器)は、壁の向こうに置かれています。30年前のプリンタが、いまはブラウザに替わっただけです。
原典のコピーと、このプラグインのコピーは、利用者の操作や出力先が同じではありません。重なるのは、「読む・書くという仕事の手順を、特定の道具から離して扱いたい」という構造です。プリンタの例をそのままブラウザへ移植するのではなく、どの知識を方針の側から外したのかを比べています。
もし出力先が一つしかなくても、その境目が役立つ場合はあります。テストでは、本物の出力先へ触らず、渡した内容を記録したいからです。本番用の実装が一つであることと、異なる環境で差し替えて試したいことは両立します。「実装が二つになったら初めて考える」と、数だけで境目を決めないようにします。
おさらい:このプラグインの四つの層
先に進む前に、このプラグインのブラウザ側のコードが、どう分かれているかを確かめておきます。連載の本編でも紹介したとおり、コードは役割ごとのレイヤーに分かれています。表示と操作を受け持つ presentation、共有の手順を受け持つ application、判断と約束を置く domain、ブラウザの機能を実際に呼ぶ infrastructure の四つです。そのほかに、起動するときに部品をつなぐ組み立て役として composition があります。
地図のいちばん大事なところは、矢印の集まり方です。application は domain を知り、infrastructure も domain を知っています。けれど、domain はほかのどの層も知りません。この境界では、application も infrastructure も domain の約束を参照します。コピーの約束(ClipboardGateway)が置かれているのも、この domain の中の gateway というフォルダです。点線の矢印は、組み立て役が起動するときにつなぐ線で、コードの import とは別のものです。この地図を手元に置きながら、次のページへ進みましょう。
考えてみる:方針のコードが、ブラウザを直接呼んでも、今のところ動いているなら困らないのでは?
動いているあいだは困りません。困るのは、変えたくなったときと、試したくなったときです。ブラウザの機能は、ブラウザの種類や権限、テストの環境によって振る舞いが変わります。方針のコードがブラウザを直接呼んでいると、そのたびに方針のコードを開くことになり、テストではブラウザの代わりを差し込む口がありません。5ページ目で、実際に直接呼ぶ形に書き換えて、テストの何件が落ちるかを数えます。
層の地図で最初に確かめたいのは、applicationからinfrastructureへ、直接の線が出ていないことです。presentationからapplicationへの線まで消す必要はありません。画面が共有の手順を呼ぶのは自然な依存です。「全部のimportがdomainだけへ向く」という意味で地図を読むと、実物のコードと合わなくなります。
組み立て役は、その地図を外から眺める位置にあります。どの口へどの実装を差すかを決めるため、約束も具体的な道具も知っています。その例外を一か所へまとめることで、方針のクラスそれぞれに具体名を書かずに済みます。どこにも具体名を書かない設計ではなく、具体名を知る役割を決めた設計です。
直接呼ぶ形でも、ブラウザのグローバルを書き換えるなどしてテストする方法はあります。ただ、その方法では環境の差し替えが広い範囲へ及び、テストどうしの干渉にも気を配ることになります。この連載では、必要な能力だけを引数で渡す形を使っています。試せる・試せないを絶対的な二択にせず、何を準備して試せるかの違いまで考えます。







このページ(1ページ目)の感想・質問・設計へのコメント