DIP――ブラウザの機能を、ポートの向こうへ置く
作成: / 公開: / 内容更新:
執筆:tamito0201 / 掲載・運営:プロマリ
丸い共有ボタンの裏側を掘る連載の第8回。SOLID の5つ目の原則 DIP を、「コピー」のボタン1つから確かめます。コピーを頼むコードはブラウザを知らず、約束(ポート)だけを知る。実行時の呼び出しは外へ、import は内へ。import の向きを数え、DI・DI コンテナ・ファクトリの役目を読み、わざと境界を破って何が止めるかまで、図16点で読み解…
まず一般的な例で:ボタンとデータベース
前のページでは、DIPを「方針のコードも、道具のコードも、あいだに置いた約束のほうを向く」とまとめました。このページでは、いったん共有ボタンから離れて、よくある小さな例で、この「向く」の意味をつかみます。題材は、ボタンを押したら、入力した文字をデータベースに保存する、という処理です。
ボタンが保存先を自分で作る形
何も考えずに書くと、たいていは次のようになります。
import { DatabaseSaver } from './DatabaseSaver';
export class Button {
private saver = new DatabaseSaver();
onClick(text: string): void {
this.saver.save(text);
}
}上から読んでいきます。1行目のimportで、Button は DatabaseSaver という具体的な保存の部品の名前を知ります。4行目では、その DatabaseSaver を自分で new して作り、持っています。6〜8行目のクリックの処理では、その保存の部品に文字を渡しています。短くて、読みやすいコードです。
困りごとは、変化が来たときに現れます。1つ目は、保存先を変えたくなったときです。「データベースではなく、ファイルに保存したい」と言われたら、ボタンのコードを開いて、1行目と4行目を書き換えることになります。ボタンの役目は「押されたら保存を頼む」ことで、それ自体は何も変わっていないのに、です。
2つ目は、ボタンだけを試したくなったときです。「クリックしたら、入力した文字がちゃんと保存に回るか」を確かめたいだけなのに、ボタンが自分で DatabaseSaver を作ってしまうので、データベースが用意できない場所ではテストが動きません。3つ目は、作る順番です。保存の担当がまだ DatabaseSaver を作り終えていなければ、ボタンの担当も具体的な保存の部品の形が決まるまで、その接続を完成させられません。
3つとも原因は同じです。ボタン(方針)が、保存の具体的なやり方(道具)を直接知っていることです。依存の矢印は、ボタンから DatabaseSaver へ、一方通行に伸びています。
データベースの図は、最初にボタンの箱の中にあるnewを探してみてください。保存の道具を選ぶ判断が、その箱の中へ入っています。だから、保存先を替える依頼が来ると、ボタンの役割が変わっていなくても、ボタンの箱を開くことになります。矢印は単なる呼び出しの線であると同時に、具体名を知っている場所も示しています。
作る順番についても、完全に手が止まるという意味ではありません。仮のクラスを置いたり、担当者どうしで形を決めたりすれば、並行して作ることはできます。ただ、具体的な保存のクラスを基準に会話する形では、その都合がボタンの側へ入りやすくなります。最初に使う側の必要を約束として決めると、実装を待たずに何を試すかを合意しやすくなります。
この短い例では、接続を閉じる処理や保存失敗の表示などを省いています。データベースを使う完成品の実装例ではなく、具体的な道具をどこで選ぶかを示す例です。図とコードを読むときは、保存先を選ぶ責任の置き場所に絞って見てください。
「保存する」という約束を挟む
そこで、ボタンと保存のあいだに、「保存する」という約束を1つ挟みます。約束には、どう保存するかは書かず、「文字列を受け取って保存する」ことだけを書きます。TypeScript では、これをインターフェースで表します。
// Saver.ts:ボタンが必要とする「保存する」という約束
export interface Saver {
save(text: string): Promise<void>;
}
// Button.ts:約束だけを知り、保存の部品は外から受け取る
import type { Saver } from './Saver';
export class Button {
constructor(private readonly saver: Saver) {}
async onClick(text: string): Promise<void> {
await this.saver.save(text);
}
}上の3行が約束です。save という名前で文字列を受け取り、終わったら知らせる(Promise<void> を返す)。それだけです。データベースという言葉は、どこにも出てきません。下の Button は、import で Saver という約束の名前だけを知ります。そして、コンストラクタの引数で、約束を満たす部品を外から受け取ります。ボタンの中に new は1つもありません。
約束へ移したコードでは、何を受け取り、いつ終わったとするかが残っています。データベースの表名や接続方法は消えました。この差を、二つの図で見比べてください。機能を丸ごと消したのではなく、使う側が頼る仕事と、その仕事を実現する道具の手順を分けています。
Promiseを返す形にしたことにも意味があります。保存の完了を待つ必要があるなら、その待ち方は使う側に見える約束の一部です。抽象にすれば時間や失敗を考えずに済むわけではありません。道具の細部を隠しながら、使う側が必要とする結果と完了の条件は隠さない。そう読むと、小さな口にも十分な意味が入っていると分かります。
// DatabaseSaver.ts:保存の部品のほうが、約束に合わせる
import type { Saver } from './Saver';
export class DatabaseSaver implements Saver {
async save(text: string): Promise<void> {
// データベースへ書き込む(中身は省略)
}
}
// main.ts:どの部品を使うかは、組み立てる場所だけが知る
const button = new Button(new DatabaseSaver());
// test:代役を渡せば、データベースなしで試せる
const saved: string[] = [];
const testButton = new Button({ save: async (t) => { saved.push(t); } });DatabaseSaver は implements Saver と書いて、約束に合わせて作られます。import しているのは、やはり Saver という約束のほうです。そして、どの部品を使うかは、最後の main.ts のような「組み立てる場所」だけが知っています。テストでは、書き込むはずの文字を配列に記録するだけの代役を渡せば、データベースが無くてもボタンを試せます。
ここで、import の矢印を並べてみます。約束を挟む前は、ボタンから DatabaseSaver へ矢印が伸びていました。挟んだ後は、ボタンから Saver へ、そして DatabaseSaver からも Saver へ、矢印が伸びています。保存の部品から出る矢印の向きが、ボタンの側へと逆になりました。これが「依存性逆転」の「逆転」です。保存の部品は、もう誰かに使われるのを待つだけの存在ではなく、ボタンが決めた約束に合わせる側へ回りました。
変化が来たときの動きも変わります。ファイルに保存したくなったら、FileSaver を1つ作って、組み立てる場所で渡す部品を替えるだけです。ボタンのコードは開きません。これは、連載2.2で読んだ OCP の「足すだけで済む」形でもあります。DIP は、OCP の差し込み口を、コードの矢印の向きから作る方法だと言えます。
この図で「逆になった」と言っているのは、DatabaseSaverが約束を参照する向きです。ボタンから出るすべての矢印を逆向きにするわけではありません。ボタンは依然として、保存を頼む側です。実行時にデータベースが勝手にボタンを呼びに来るようになる、という話でもありません。
また、FileSaverを足すだけで済むのは、同じ保存の約束を満たせる場合です。ファイルへ保存した後にURLを返したい、複数の保存をまとめて確定したい、といった新しい要求が来れば、約束のほうも見直します。DIPは、将来の要求をすべて予知する方法ではありません。現在の方針を、現在の道具の詳細から切り離すための向きです。
約束は、使う側の都合で書く
もう1つ、見落としやすい点があります。約束(Saver)を、どちらの都合で書くかです。
保存の担当に任せると、約束は「データベースの接続を開く」「表の名前を指定する」「行を挿入する」のように、データベースの手順をそのまま並べたものになりがちです。これでは、名前がインターフェースになっただけで、ボタンは相変わらずデータベースの都合を知っています。ファイルに替えようとしても、「表の名前」をファイルでどう扱うのか、と困ってしまいます。
DIP の約束は、使う側(ボタン)が「何をしてほしいか」で書きます。ボタンが欲しいのは「この文字を保存しておいて」だけなので、約束も save(text) の1つだけです。約束を使う側の近くに置くのも、そのためです。このプラグインで、ClipboardGateway がブラウザの実装の隣(infrastructure)ではなく、方針の側(domain)に置かれているのも、同じ理由です。
DIP を守らないとどうなるかも、ここでまとめておきます。保存先の変更がボタンの書き換えになる。ボタンを試すのに本物の道具が要る。道具の具体的な形に合わせて、方針のコードを作る必要がある。そして、道具が増えるたびに、方針のコードに「この場合はこちら」の分岐が積み上がる。Martin の Copy の例で、出力先ごとに if 文が増えていったのと同じ姿です。
考えてみる:約束を挟めば、どんな道具でも差し替えられるの?
約束の形に合う道具なら差し替えられますが、合わない道具は差し替えられません。Saver の約束は「文字列を1つ受け取って保存する」だけなので、画像を保存したくなったら、約束そのものを直すことになります。それに、差し替えた道具が約束の形だけを満たして、中身の約束(保存したら成功を返す、失敗したら失敗を返す)を破っていたら、使う側は壊れます。形だけでなく中身の約束まで守らせるのは、前々回の連載2.3で読んだ LSP の話です。
口を作っても、道具の型を荷物として持ち込んでいないか
インターフェースを置いたのに、道具を替えにくいことがあります。次の図では、差し込み口を通る荷物の中に、データベース専用の型が混ざった場合を描きました。約束の名前だけでなく、中を通る型まで見るための説明例です。
上の口は、DatabaseRowを持ってくるよう求めています。名前はSaverでも、使う側は、その行オブジェクトを作るためにデータベース側の型を知る必要があります。図の赤い荷札が、口をまたいで残った依存です。約束を経由しているように見えても、道具の都合が荷物の形としてこちらへ戻ってきています。
下の口では、使う側が扱う文字列を渡します。データベースの行へ変換する仕事は、向こう側の実装が引き受けます。これなら、保存先をファイルへ替えたときに、使う側が表の列を組み立てる理由はありません。道具を隠すとは、クラス名を消すだけでなく、道具固有の準備を使う側へ求めないことでもあります。
いつでも文字列だけにすればよい、という意味ではありません。仕事に必要な情報が複数あるなら、使う側の言葉でまとまりを定義できます。記事のURLとタイトルなら、その二つを表す値として渡せます。重要なのは、特定のライブラリが都合よく返すオブジェクトを、そのまま契約の正本にしていないかという点です。
例外の型にも同じ問いを当てられます。方針の側が、特定のデータベース製品のエラー番号を一つずつ判定しないと仕事を進められないなら、詳細が境目を越えています。どの失敗を利用者へ伝えたいのかを先に整理し、必要な失敗の意味を契約へ表す方法を考えます。これはこの例から広げた検討であり、現在のClipboardGatewayへ新しいエラー型を追加したという話ではありません。
荷札の練習として、保存先をファイルへ替える場面を想像します。DatabaseRowを要求する口のままなら、ファイルへ保存するだけなのに、データベースの行を作る準備が残ります。使わない道具の言葉を、別の道具へ翻訳するためだけに持ち歩く形です。
文字列を渡す口なら、ファイル側はその文字列をファイルの形式へ変換できます。データベース側も、同じ文字列を自分の行へ変換できます。変換する作業が消えたわけではありません。どちらの道具を使うかに応じた変換を、その道具を知る場所へ置いたのです。DIPによる整理は、必要な複雑さを正しい持ち主へ渡す作業でもあります。
ただし、業務として表の行そのものを操作する道具を作っているなら、行を表す型が必要な場合もあります。ここでは、ボタンの目的が文字の保存であるという前提から判断しています。型の名前だけで良し悪しを決めず、その情報を使う側の仕事として説明できるかを見ます。
壁の図をレビューするときには、矢印の先だけでなく、矢印に載っている荷札も読んでみてください。引数、返り値、失敗の表し方の三つです。どれも方針の言葉で説明できれば、具体的な道具の変更を向こう側で引き受けやすくなります。フォルダの置き場所だけでは見つからない依存を、ここで確かめられます。
このページの「守っていない例・守っている例・守らないとどうなるか」という進め方と、ボタンとデータベース保存の題材は、NakuRei さんの本『SOLID原則完全に理解した!になるための本』の「依存性逆転の原則」の章を参考に、この連載の TypeScript で書き直しました。Martin の原典は、C++ Report のコラムThe Dependency Inversion Principle(1996、Web Archive に残る写し)で読めます。









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