PROMARI JOURNAL

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

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

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

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

PASS IT ON

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

まず一般的な例で:ボタンとデータベース

前のページでは、DIPを「方針のコードも、道具のコードも、あいだに置いた約束のほうを向く」とまとめました。このページでは、いったん共有ボタンから離れて、よくある小さな例で、この「向く」の意味をつかみます。題材は、ボタンを押したら、入力した文字をデータベースに保存する、という処理です。

ボタンが保存先を自分で作る形

何も考えずに書くと、たいていは次のようになります。

TypeScriptButton.ts(説明用に書いた例・保存先を直接知っている形)
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行目のクリックの処理では、その保存の部品に文字を渡しています。短くて、読みやすいコードです。

図2.1-1 ボタンが保存先を直接呼ぶと
図2.1-1 ボタンが保存先を直接呼ぶと。説明例。ボタンが具体的な保存先を知っていると、保存先の都合がそのままボタンの書き換えになります。図2.1-1 ボタンが保存先を直接呼ぶと。説明例。ボタンが具体的な保存先を知っていると、保存先の都合がそのままボタンの書き換えになります。

困りごとは、変化が来たときに現れます。1つ目は、保存先を変えたくなったときです。「データベースではなく、ファイルに保存したい」と言われたら、ボタンのコードを開いて、1行目と4行目を書き換えることになります。ボタンの役目は「押されたら保存を頼む」ことで、それ自体は何も変わっていないのに、です。

2つ目は、ボタンだけを試したくなったときです。「クリックしたら、入力した文字がちゃんと保存に回るか」を確かめたいだけなのに、ボタンが自分で DatabaseSaver を作ってしまうので、データベースが用意できない場所ではテストが動きません。3つ目は、作る順番です。保存の担当がまだ DatabaseSaver を作り終えていなければ、ボタンの担当も作り始められません。

3つとも原因は同じです。ボタン(方針)が、保存の具体的なやり方(道具)を直接知っていることです。依存の矢印は、ボタンから DatabaseSaver へ、一方通行に伸びています。

「保存する」という約束を挟む

そこで、ボタンと保存のあいだに、「保存する」という約束を1つ挟みます。約束には、どう保存するかは書かず、「文字列を受け取って保存する」ことだけを書きます。TypeScript では、これをインターフェースで表します。

TypeScriptSaver.ts と Button.ts(説明用に書いた例・約束を挟んだ形)
// 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つもありません。

TypeScriptDatabaseSaver.ts と、組み立てる場所(説明用に書いた例)
// 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 のような「組み立てる場所」だけが知っています。テストでは、書き込むはずの文字を配列に記録するだけの代役を渡せば、データベースが無くてもボタンを試せます。

図2.2-1 抽象を挟むと、矢印の向きが逆になる
図2.2-1 抽象を挟むと、矢印の向きが逆になる。説明例。保存の担当が、ボタンの決めた約束に合わせる側へ回ります。依存の矢印が逆になるので「逆転」と呼びます。図2.2-1 抽象を挟むと、矢印の向きが逆になる。説明例。保存の担当が、ボタンの決めた約束に合わせる側へ回ります。依存の矢印が逆になるので「逆転」と呼びます。

ここで、import の矢印を並べてみます。約束を挟む前は、ボタンから DatabaseSaver へ矢印が伸びていました。挟んだ後は、ボタンから Saver へ、そして DatabaseSaver からも Saver へ、矢印が伸びています。保存の部品から出る矢印の向きが、ボタンの側へと逆になりました。これが「依存性逆転」の「逆転」です。保存の部品は、もう誰かに使われるのを待つだけの存在ではなく、ボタンが決めた約束に合わせる側へ回りました。

変化が来たときの動きも変わります。ファイルに保存したくなったら、FileSaver を1つ作って、組み立てる場所で渡す部品を替えるだけです。ボタンのコードは開きません。これは、連載2.2で読んだ OCP の「足すだけで済む」形でもあります。DIP は、OCP の差し込み口を、コードの矢印の向きから作る方法だと言えます。

約束は、使う側の都合で書く

もう1つ、見落としやすい点があります。約束(Saver)を、どちらの都合で書くかです。

保存の担当に任せると、約束は「データベースの接続を開く」「表の名前を指定する」「行を挿入する」のように、データベースの手順をそのまま並べたものになりがちです。これでは、名前がインターフェースになっただけで、ボタンは相変わらずデータベースの都合を知っています。ファイルに替えようとしても、「表の名前」をファイルでどう扱うのか、と困ってしまいます。

DIP の約束は、使う側(ボタン)が「何をしてほしいか」で書きます。ボタンが欲しいのは「この文字を保存しておいて」だけなので、約束も save(text) の1つだけです。約束を使う側の近くに置くのも、そのためです。このプラグインで、ClipboardGateway がブラウザの実装の隣(infrastructure)ではなく、方針の側(domain)に置かれているのも、同じ理由です。

DIP を守らないとどうなるかも、ここでまとめておきます。保存先の変更がボタンの書き換えになる。ボタンを試すのに本物の道具が要る。道具ができるまで方針のコードを作れない。そして、道具が増えるたびに、方針のコードに「この場合はこちら」の分岐が積み上がる。Martin の Copy の例で、出力先ごとに if 文が増えていったのと同じ姿です。

考えてみる:約束を挟めば、どんな道具でも差し替えられるの?

約束の形に合う道具なら差し替えられますが、合わない道具は差し替えられません。Saver の約束は「文字列を1つ受け取って保存する」だけなので、画像を保存したくなったら、約束そのものを直すことになります。それに、差し替えた道具が約束の形だけを満たして、中身の約束(保存したら成功を返す、失敗したら失敗を返す)を破っていたら、使う側は壊れます。形だけでなく中身の約束まで守らせるのは、前々回の連載2.3で読んだ LSP の話です。

このページの「守っていない例・守っている例・守らないとどうなるか」という進め方と、ボタンとデータベース保存の題材は、NakuRei さんの本『SOLID原則完全に理解した!になるための本』の「依存性逆転の原則」の章を参考に、この連載の TypeScript で書き直しました。Martin の原典は、C++ Report のコラムThe Dependency Inversion Principle(1996、Web Archive に残る写し)で読めます。

COMMENTS
コメント…

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

PASS IT ON

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

この記事を書いた人

Takaomi Murasaki

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

公開記事 10 件

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

FROM INSIGHT TO IMPACT

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

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

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

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

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

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

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

ご相談後の流れ

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