Bridge:出力形式とチケット種別の分離
📋 この記事の目次
TicketCraftには、チケットの「概要レポート」「詳細レポート」を、画面表示・PDF出力など複数の形式で出せる機能があります。今のコードは、種類と出力形式の組み合わせごとにクラスが1つずつ用意されています。
public class SummaryScreenReport {
public void show(String title) {
System.out.println("[画面] 概要: " + title);
}
}
public class DetailPdfReport {
public void show(String title, String detail) {
System.out.println("[PDF] 詳細: " + title + " - " + detail);
}
}
これに加えてSummaryPdfReport・DetailScreenReportもあり、すでに4クラスです。ここに「Slack出力」を追加すると6クラス、将来「担当者別レポート」のような3つ目の種類を追加すると9クラスと、組み合わせの数だけクラスが掛け算式に増えていきます。
Bridgeは何のためにあるか
Bridgeは、「何を(レポートの種類)」と「どう出すか(出力形式)」という2つの軸を分離し、それぞれ独立に増やせるようにする」 パターンです。洋服の「デザイン」と「サイズ」を別々の軸で管理すれば、デザイン×サイズの組み合わせごとに服を1着ずつ縫い直す必要がなくなるのと同じ発想です。
Before:種類と出力形式が1つのクラスに固まっている
public class Main {
public static void main(String[] args) {
SummaryScreenReport screenSummary = new SummaryScreenReport();
screenSummary.show("ログイン画面が真っ白になる");
DetailPdfReport pdfDetail = new DetailPdfReport();
pdfDetail.show("ログイン画面が真っ白になる", "Chromeのバージョン120で再現");
}
}
実行結果:
[画面] 概要: ログイン画面が真っ白になる
[PDF] 詳細: ログイン画面が真っ白になる - Chromeのバージョン120で再現
動作はしていますが、新しい出力形式(Slack)を追加するには、SummarySlackReport・DetailSlackReportという2つの新規クラスが必要です。
After:出力形式を切り離し、組み合わせて使う
出力形式(実装側)を、共通のインターフェースとして切り出します。
public interface ReportFormatter {
void output(String content);
}
public class ScreenFormatter implements ReportFormatter {
@Override
public void output(String content) {
System.out.println("[画面] " + content);
}
}
public class PdfFormatter implements ReportFormatter {
@Override
public void output(String content) {
System.out.println("[PDF] " + content);
}
}
public class SlackFormatter implements ReportFormatter {
@Override
public void output(String content) {
System.out.println("[Slack] " + content);
}
}
レポートの種類(抽象側)は、出力形式を「橋(Bridge)」として保持するだけにします。
public abstract class TicketReport {
protected final ReportFormatter formatter;
protected TicketReport(ReportFormatter formatter) {
this.formatter = formatter;
}
public abstract void show(String title, String detail);
}
public class SummaryReport extends TicketReport {
public SummaryReport(ReportFormatter formatter) {
super(formatter);
}
@Override
public void show(String title, String detail) {
formatter.output("概要: " + title);
}
}
public class DetailReport extends TicketReport {
public DetailReport(ReportFormatter formatter) {
super(formatter);
}
@Override
public void show(String title, String detail) {
formatter.output("詳細: " + title + " - " + detail);
}
}
public class Main {
public static void main(String[] args) {
TicketReport screenSummary = new SummaryReport(new ScreenFormatter());
screenSummary.show("ログイン画面が真っ白になる", "");
TicketReport pdfDetail = new DetailReport(new PdfFormatter());
pdfDetail.show("ログイン画面が真っ白になる", "Chromeのバージョン120で再現");
// Slackという新しい出力形式を追加しても、新しいクラスは1つだけでよい
TicketReport slackSummary = new SummaryReport(new SlackFormatter());
slackSummary.show("ダークモードが欲しい", "");
}
}
実行結果:
[画面] 概要: ログイン画面が真っ白になる
[PDF] 詳細: ログイン画面が真っ白になる - Chromeのバージョン120で再現
[Slack] 概要: ダークモードが欲しい
「レポートの種類」(SummaryReport・DetailReport)と「出力形式」(ScreenFormatter・PdfFormatter・SlackFormatter)が、それぞれ独立に増やせるクラスになりました。種類が2つ、形式が3つでも、必要なクラスは2+3=5つで済みます(組み合わせの数である2×3=6を用意する必要はありません)。
🧓 ベテランの現場コラム Bridgeは「継承ではなく、オブジェクトを持たせて委譲する」という考え方の代表例です。「このクラス、似たようなクラスが何個もあるな」と感じたら、その違いが実は2つの軸(今回で言えば種類と出力形式)に分解できないか考えてみましょう。ただし、軸が最初から1つしかなく、今後も増える見込みがない場合にまでBridge化すると、かえって間接参照が増えて読みにくくなることもあります。
こんな場面で使う/使わない
- 使う: 「何をするか」と「どう実現するか」という2つの軸があり、両方が今後増える可能性がある場合
- 使わない: 軸が1つしかなく、組み合わせの爆発が起きていない場合
まとめ
- Bridgeは「何を」と「どう出すか」の2つの軸を分離し、独立に拡張できるようにするパターン
- 組み合わせの数だけクラスが増える「クラスの爆発」を防げる
- 軸が実際に複数あり、両方が今後も増える見込みがある場合に検討する
🎓 理解度テスト
学んだ内容を確認しましょう。全3問。