java-recipes

ホーム デザインパターン › DP-07

DP-07: Bridge パターン

抽象化と実装を別の継承ツリーに分離し、それぞれを独立して変更・拡張できるパターンです。 「通知の種類(緊急・レポート)」と「送信手段(メール・SMS)」を独立させることで、 両者の組み合わせを自由に扱えます。

Bridge パターンとは

Bridge(ブリッジ)パターンは、クラスを「抽象(何をするか)」と「実装(どうやるか)」の 2つの独立した継承ツリーに分けるパターンです。 抽象クラスが実装インターフェースへの参照を持ち(ブリッジ)、実行時に差し替えられます。

Bridge パターンが役立つ場面

  • 多次元の変化: 「通知種別 × 送信手段」のように2軸で変化するとき、単純な継承だと組み合わせ爆発する
  • 実装の差し替え: メール送信をSMS送信に変えるなど、実行時に実装を切り替えたい場面
  • 独立した拡張: 通知種別と送信手段を別々のチームが独立して拡張・変更できる

継承による問題と Bridge の解決策

継承だけで「緊急メール通知」「緊急SMS通知」「レポートメール通知」「レポートSMS通知」を 実装すると4クラス必要です。通知種別が3種・送信手段が4種になると 3×4=12 クラスが必要になります。

Bridge パターンでは通知種別クラス(抽象側)+ 送信手段クラス(実装側)を組み合わせるだけなので、 3+4=7 クラスで済みます。

サンプルコード

BridgePatternSample.java
public class BridgePatternSample {

    // 実装インターフェース(Implementor): メッセージ送信手段
    interface MessageSender {
        void send(String to, String subject, String body);
    }

    // 具体実装1: メール送信
    static class EmailSender implements MessageSender {
        @Override
        public void send(String to, String subject, String body) {
            System.out.println("[メール送信]");
            System.out.println("  宛先: " + to);
            System.out.println("  件名: " + subject);
            System.out.println("  本文: " + body);
        }
    }

    // 具体実装2: SMS 送信
    static class SmsSender implements MessageSender {
        @Override
        public void send(String to, String subject, String body) {
            System.out.println("[SMS 送信]");
            System.out.println("  宛先: " + to);
            // SMS は件名なし・本文のみ
            System.out.println("  内容: " + body);
        }
    }

    // 抽象クラス(Abstraction): 通知の種類
    static abstract class Notification {
        protected MessageSender sender; // 実装への参照(ブリッジ)

        Notification(MessageSender sender) {
            this.sender = sender;
        }

        abstract void notify(String recipient, String message);
    }

    // 具体抽象1: 緊急通知
    static class UrgentNotification extends Notification {
        UrgentNotification(MessageSender sender) {
            super(sender);
        }

        @Override
        void notify(String recipient, String message) {
            sender.send(recipient, "【緊急】" + message, "至急確認してください: " + message);
        }
    }

    // 具体抽象2: 定期レポート通知
    static class ReportNotification extends Notification {
        ReportNotification(MessageSender sender) {
            super(sender);
        }

        @Override
        void notify(String recipient, String message) {
            sender.send(recipient, "定期レポート", "本日のレポートをお届けします:\n" + message);
        }
    }

    public static void main(String[] args) {
        // 緊急通知 × メール
        Notification urgentEmail = new UrgentNotification(new EmailSender());
        urgentEmail.notify("admin@example.com", "サーバー障害");

        System.out.println();

        // 緊急通知 × SMS(送信手段だけ差し替え)
        Notification urgentSms = new UrgentNotification(new SmsSender());
        urgentSms.notify("090-1234-5678", "サーバー障害");

        System.out.println();

        // 定期レポート × メール(通知種類だけ差し替え)
        Notification reportEmail = new ReportNotification(new EmailSender());
        reportEmail.notify("manager@example.com", "売上: ¥1,000,000 / アクセス: 500件");
    }
}

Java 8 では interface(Implementor)と abstract class(Abstraction)を組み合わせて Bridge パターンを実装します。Abstraction は Implementor への参照をフィールドに持ち(ブリッジ)、コンストラクタで受け取ることで実行時に差し替えられます。

よくあるミス・注意点

⚠️ Adapter パターンと混同してしまう

Adapter パターンは「既存のインターフェースを別のインターフェースに変換する」パターンです。 Bridge パターンは「設計の最初から抽象と実装を分離する」パターンです。 Adapter は後付けの変換、Bridge は最初から意図した分離、という違いがあります。

⚠️ 抽象クラスで実装(Implementor)を直接インスタンス化してしまう

抽象クラス内で new EmailSender() のように 具体実装クラスを直接作ると、Bridge の恩恵がなくなります。 実装はコンストラクタの引数で外から渡す(依存性の注入)ことで、柔軟な差し替えが可能になります。

⚠️ 変化の軸が1つしかない場面で使うと過設計になる

Bridge パターンは「2つの独立した変化軸がある」場合に効果を発揮します。 通知の種類が1種類しかない、または送信手段が1つだけなら、 単純なクラス継承や Strategy パターンのほうがシンプルな場合があります。 パターンの適用は「将来の拡張性」を見据えて判断しましょう。

テストする観点

  • UrgentNotification に EmailSender を渡したとき、件名に「【緊急】」が含まれること
  • UrgentNotification に SmsSender を渡したとき、同じメッセージが SMS 形式で送信されること(送信手段のみ変化)
  • ReportNotification に EmailSender を渡したとき、件名が「定期レポート」であること
  • 同じ EmailSender インスタンスを UrgentNotification と ReportNotification で共有しても、それぞれ独立して動作すること
  • recipient に空文字を渡してもエラーにならないこと(境界値)
  • message に null を渡したとき NullPointerException が発生しないこと(境界値)

GitHub でソースコードを見る →