コーディング · デザインパターン
Bridge パターンを例でわかりやすく解説
結論
Bridge パターンは、1つの大きなクラス階層を、抽象と実装という2つの別々の階層に分け、合成でつなぐ構造パターンです。抽象(たとえばレポートの種類)が実装オブジェクト(たとえば出力のレンダラー)への参照を保持し、低レベルの作業をそこに委譲します。これにより両側が独立して成長でき、3種類のレポートと3つのレンダラーなら、サブクラスの組み合わせの9クラスではなく、6クラスで済みます。
目的
抽象をその実装から切り離し、両者が独立して変化できるようにします。Bridge パターンは、Gang of Four(GoF)の書籍にある7つの構造パターンの1つで、同書ではHandle/Bodyとも呼ばれています。
解決する問題
アラートとリマインダーの2種類のメッセージを、メールとSMSの2つのチャネルで送るアプリを想像してください。継承でモデル化すると、EmailAlert、SmsAlert、EmailReminder、SmsReminder ができます。プッシュのチャネルとダイジェストのメッセージを加えると、9クラスが必要になります。どちらの側でも新しい種類を加えるたびに、総数が掛け算で増えていきます。
本当の問題は、クラス階層が2つの独立した問いを混ぜていることです。メッセージが何であるかと、それをどう配信するかです。Bridge パターンはそれを2つの階層に分けて参照でつなぐので、総数が掛け算ではなく足し算で増えます。
| 設計 | 2メッセージ × 2チャネル | 3メッセージ × 3チャネル | 4メッセージ × 4チャネル |
|---|---|---|---|
| 組み合わせごとにサブクラス | 4クラス | 9クラス | 16クラス |
| Bridge パターン | 4クラス | 6クラス | 8クラス |
構造と登場要素
| 要素 | 役割 | 例では |
|---|---|---|
| Abstraction | クライアントが使う高レベルのインターフェイスで、Implementorへの参照を保持します。 | Message |
| RefinedAbstraction | 抽象を拡張して、バリエーションのある振る舞いを加えます。 | Alert, Reminder |
| Implementor | 低レベルの操作のインターフェイスです。 | Sender |
| ConcreteImplementor | 1つのプラットフォームまたは技術向けに、低レベルの操作を実装します。 | EmailSender, SmsSender |
TypeScriptでの実装
// Implementor: how a message is delivered.
interface Sender {
deliver(to: string, subject: string, body: string): string;
}
class EmailSender implements Sender {
deliver(to: string, subject: string, body: string) { return `email ${to} | ${subject} | ${body}`; }
}
class SmsSender implements Sender {
deliver(to: string, subject: string, body: string) { return `sms ${to} | ${subject}: ${body}`.slice(0, 40); }
}
// Abstraction: what the message is. It holds the bridge to a Sender.
abstract class Message {
constructor(protected sender: Sender) {}
abstract send(to: string): string;
}
class Alert extends Message {
constructor(sender: Sender, private problem: string) { super(sender); }
send(to: string): string {
return this.sender.deliver(to, "ALERT", this.problem.toUpperCase());
}
}
class Reminder extends Message {
constructor(sender: Sender, private task: string, private due: string) { super(sender); }
send(to: string): string {
return this.sender.deliver(to, "Reminder", `${this.task} by ${this.due}`);
}
}
console.log(new Alert(new SmsSender(), "disk full").send("+100")); // sms +100 | ALERT: DISK FULL
console.log(new Reminder(new EmailSender(), "Pay invoice", "Friday").send("[email protected]"));
// email [email protected] | Reminder | Pay invoice by FridayPushSender の追加は1クラスで済み、アラートとリマインダーの両方でそのまま動作します。Digest メッセージの追加も1クラスで、すべての送信者でそのまま動作します。送信者は単なるコンストラクタの引数なので、ユーザーの設定に応じてなど、実行時に選ぶこともできます。
使うべきとき、使わないとき
次の場合はBridge パターンを使います。
- クラスが2つ以上の独立した次元で変化し、継承だと組み合わせごとにクラスを作ることになる場合。
- 実装を実行時に切り替えたい、または1つの実装オブジェクトを複数の抽象で共有したい場合。
- 複数のプラットフォーム(OS、レンダリングのバックエンド、データベースエンジン)で動く必要があるコードを作り、プラットフォームの詳細を別の1つの階層にまとめたい場合。
次の場合は避けます。
- 変化の次元が1つだけの場合。実装が複数ある単純なインターフェイスで十分です。
- 実装が1つしかないのに「将来のために」導入する場合。余分な層のせいで、まだ利益がないままコードが読みにくくなります。
実際の利用例
- JavaのJDBCはよく知られた例です。アプリケーションコードはJDBC APIに対して書かれ、各データベースベンダーが低レベルの操作を実装するドライバーを提供します。両者は独立して進化します。
- グラフィックスやUIのツールキットは、プラットフォームに依存しないウィジェットや図形のAPIを、プラットフォーム固有の描画バックエンドの上に置きます。GoFの書籍自身の例は、ウィンドウシステムごとに別々の
WindowImp実装を持つ、Windowの抽象です。 - ロギングライブラリはよく、コードが呼び出すロガーのAPIと、ファイル、コンソール、ネットワークサービスに書き込むハンドラーやアペンダーを分離しています。
関連するパターン
| パターン | Bridge パターンとの関係 |
|---|---|
| Adapter パターン | 形は似ていますが、設計済みの無関係な既存クラスを後から協調させるものです。Bridge パターンは、クラスを書く前に計画します。 |
| Strategy パターン | こちらも、交換可能なオブジェクトに委譲します。Strategy パターンはアルゴリズムの入れ替えが目的で、Bridge パターンは抽象の階層全体を実装の階層から分離することが目的です。 |
| Abstract Factory パターン | Bridgeに適した実装オブジェクト、たとえば正しいプラットフォームのバックエンドを作成して設定できます。 |
| Composite パターン | Bridge パターンも継承より合成に基づいていますが、Composite パターンはオブジェクトの木構造に関するもので、2つの独立した階層に関するものではありません。 |
よくある質問
Bridge パターンでいう「実装」とは何ですか?
クラスの中のコードのことではありません。抽象が委譲する、低レベルでプラットフォーム固有の作業を行うオブジェクトの、独立した階層を指します。
Bridge パターンとStrategy パターンの違いは何ですか?
構造はほぼ同じです。Strategy パターンはオブジェクト内の1つのアルゴリズムを入れ替えることに焦点を当て、Bridge パターンは2つの階層全体を分離して、それぞれを独立に拡張できるようにします。
Bridge パターンとAdapter パターンの違いは何ですか?
Bridge パターンは、両側が変化できるよう事前に設計します。Adapter パターンは、既存の互換性のないクラスをインターフェイスに合わせるために、後から追加します。
Bridge パターンは依存性の注入と同じですか?
いいえ。Bridge パターンは2つの階層を構成する方法です。依存性の注入は実装オブジェクトを供給する方法で、抽象にそのImplementorを与える自然な手段です。