
Adapterと似ているデザインパターンの、Bridgeは、
抽象(機能の定義)と実装(具体的な処理)を分離して、両者を独立して拡張できるようにすることに長けています。
イメージとしては、「リモコン」と「テレビ」を別々に進化できるようにするために分離すると言うこと頭に想像してみてください。
リモコン(抽象)がテレビ(実装)に橋(Bridge)をかけて操作する感じです。
よく使う場面
・機能(抽象)と、プラットフォームや出力方法(実装)を分離したいとき
・同じ機能を「複数の環境(Web/CLI/モバイル)」で使いたいとき
・継承が増えすぎて、クラス構造が複雑になってきたとき
具体的な機能サンプル
・UIテーマと描画エンジンの分離(Light/Dark × Canvas/SVG)
・通知機能(メール/Slack/LINEなどの送信先)
・デバイス制御(コントローラ → 各ハードウェア)
サンプルコード
メッセージ送信する機能を、出力先別(Console or File)で切り替えます。
実装層(Implementation)
class MessageSender {
send(message) {
throw new Error("send()を実装してください");
}
}
具体的な実装クラス
class ConsoleSender extends MessageSender {
send(message) {
console.log(`[Console] ${message}`);
}
}
class FileSender extends MessageSender {
send(message) {
console.log(`[File] ${message}`); // 実際はファイル出力処理を行う想定
}
}
抽象層(Abstraction)
class Message {
constructor(sender) {
this.sender = sender; // 実装を橋渡しする
}
sendMessage(message) {
this.sender.send(message);
}
}
拡張された抽象クラス
class AlertMessage extends Message {
sendMessage(message) {
super.sendMessage(`【ALERT】${message}`);
}
}
実行例
const consoleMsg = new AlertMessage(new ConsoleSender());
consoleMsg.sendMessage("システムエラー発生!");
// → [Console] 【ALERT】システムエラー発生!
const fileMsg = new AlertMessage(new FileSender());
fileMsg.sendMessage("ログを保存しました。");
// → [File] 【ALERT】ログを保存しました。
解説ポイント
Message(抽象)と MessageSender(実装)を分離。
抽象が実装を直接「new」せず、外部から注入(依存注入)する。
どちらも独立して変更できる:
出力方法(Console/File)を増やしても抽象に影響しない。
メッセージ形式(Alert/Info)を増やしても実装に影響しない。
メリット
・抽象と実装を独立して拡張可能。
・継承構造をシンプルに保てる。
・実装の差し替え(DI的設計)が容易。
・テストが容易になる。
デメリット
・クラス数が増える。
・構造が少し複雑になる。
・抽象と実装の依存関係設計が難しい。
・初期構築コストが高い。
Adapterとの違い
| 項目 |
Adapter |
Bridge |
| 目的 |
互換性を持たせる |
構造を分離して柔軟に拡張 |
| タイミング |
既存コードに後付けで適応 |
設計段階で導入 |
| 関連性 |
異なるインターフェースをつなぐ |
抽象と実装を橋渡しする |
アンチパターン
1. 無理に抽象と実装を分けすぎる(小規模アプリにBridgeを導入)
// 「たった一種類の送信しかない」アプリなのに、無理やりBridge化
class Sender {
send(msg) {
throw new Error("send()を実装してください");
}
}
class ConsoleSender extends Sender {
send(msg) {
console.log(`[Console] ${msg}`);
}
}
class Message {
constructor(sender) {
this.sender = sender;
}
sendMessage(msg) {
this.sender.send(msg);
}
}
// --- 実行 ---
const message = new Message(new ConsoleSender());
message.sendMessage("Hello!");
問題点
・Sender / ConsoleSender / Message の3クラスに分かれているが、実際は console.log() 1行で済む処理。
・「分離による柔軟性」より、「クラスの増加による複雑さ」が勝っている。
・小規模・単用途では オーバーエンジニアリング。
修正版 : シンプルで十分
class Message {
send(msg) {
console.log(`[Console] ${msg}`);
}
}
2. 抽象と実装が過剰に依存している(Bridgeの意味を失っている)
class Sender {
send(msg) {
throw new Error("send()を実装してください");
}
}
class EmailSender extends Sender {
send(msg) {
console.log(`[Email送信] ${msg}`);
}
}
class Message {
constructor(sender) {
this.sender = sender;
}
sendMessage(msg) {
// ここで実装クラスを直接判定している → 依存している!
if (this.sender instanceof EmailSender) {
console.log("SMTP接続中...");
}
this.sender.send(msg);
}
}
// --- 実行 ---
const message = new Message(new EmailSender());
message.sendMessage("アラート通知!");
問題点
・抽象層(Message)が、実装層(EmailSender)に依存している。
・これではBridgeの「分離による独立性」が完全に崩壊。
・新しいSenderを追加するたびに、Messageを修正する羽目になる。
修正版 : 依存を逆転させる
class Sender {
connect() {}
send(msg) {}
}
class EmailSender extends Sender {
connect() {
console.log("SMTP接続中...");
}
send(msg) {
console.log(`[Email送信] ${msg}`);
}
}
class Message {
constructor(sender) {
this.sender = sender;
}
sendMessage(msg) {
this.sender.connect();
this.sender.send(msg);
}
}
3. BridgeとStrategyを混同している
class Message {
constructor(type) {
this.type = type;
}
send(msg) {
if (this.type === "console") {
console.log(`[Console] ${msg}`);
} else if (this.type === "email") {
console.log(`[Email] ${msg}`);
} else {
throw new Error("Unknown type");
}
}
}
問題点
・「if文で実装を切り替える」=それはBridgeではなくStrategyパターン。
・実装を「注入」せず、「内部で条件分岐」しているため、抽象と実装が分離していない。
あとがき
小規模なシステムで、このデザインパターンを使うと、オーバーエンジニアリングになる場合があるので、
オススメは、中規模以上で使うのがいいようです。
過剰依存などの状態になりやすいアンチパターンもあるので、色々なコードをレビューしてブリッジを理解するといいですね。
個人的には、クラス継承はあまり使わなので、ブリッジを使うことはあまりないのですが、使えるようになっておくことに越したことはありませんね。
0 件のコメント:
コメントを投稿