Javaを鍛える|デザインパターン全23回 #18

Mediatorパターン|UI部品同士の直接参照を仲介者に集約する

  • Java
  • デザインパターン
  • Mediator
  • Javaを鍛える

📋 この記事の目次
  1. このパターンは何のためにあるか
  2. Before
  3. After
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftのチケット一覧画面には「フィルタ欄」「一覧パネル」「詳細パネル」の3つのUI部品があり、フィルタを変えると一覧が更新され、一覧で選択すると詳細が表示されるという連携処理があります。しかし今のコードは、各部品同士が直接参照し合う状態になっており、FilterBarTicketListPanelTicketDetailPanelの両方を直接呼び出し、TicketListPanelFilterBarを直接参照しています。部品間の依存関係が相互に絡み合い、フィルタ条件の変更だけで複数箇所に修正が必要な状態になっていました。

このパターンは何のためにあるか

Mediatorパターンは、複数のオブジェクト間の通信を1つの仲介役オブジェクトに集約することで、部品同士の直接参照を取り除くデザインパターンです。アパートの住人が互いに直接連絡せず、管理会社を通じて伝言や依頼を行うのと似ています。各部品は仲介者にだけイベントを送り、仲介者が適切に他の部品へ伝達する役割を担います。

Before

以下のように、3つのUIクラスが互いを直接参照し合うコードです。

import java.util.List;

public class FilterBar {
    private TicketListPanel listPanel;
    private TicketDetailPanel detailPanel;

    public void setDependencies(TicketListPanel lp, TicketDetailPanel dp) {
        this.listPanel = lp;
        this.detailPanel = dp;
    }

    public String getFilterCriteria() { return "status=OPEN"; }

    public void onFilterChanged(String criteria) {
        listPanel.onFilterChange(criteria);
        detailPanel.refresh();
    }
}

public class TicketListPanel {
    private FilterBar filterBar;

    public void setDependencies(FilterBar fb) { this.filterBar = fb; }

    public List<String> getTicketIds() { return List.of("TK-001", "TK-002"); }

    public String getSelectedId() { return "TK-001"; }

    public void onFilterChange(String criteria) {
        System.out.println("[一覧パネル] フィルタ適用: " + criteria);
    }
}

public class TicketDetailPanel {
    private TicketListPanel listPanel;

    public void setDependencies(TicketListPanel lp) { this.listPanel = lp; }

    public String getTicketDetails() { return "TK-001 details"; }

    public void refresh() {
        System.out.println("[詳細パネル] 再描画しました");
    }
}

public class BeforeMain {
    public static void main(String[] args) {
        FilterBar filterBar = new FilterBar();
        TicketListPanel listPanel = new TicketListPanel();
        TicketDetailPanel detailPanel = new TicketDetailPanel();

        // 3部品それぞれに、互いへの参照を個別に配線する必要がある
        filterBar.setDependencies(listPanel, detailPanel);
        listPanel.setDependencies(filterBar);
        detailPanel.setDependencies(listPanel);

        filterBar.onFilterChanged(filterBar.getFilterCriteria());
        System.out.println(listPanel.getTicketIds());
        System.out.println(detailPanel.getTicketDetails());
    }
}

このコードの問題点は、FilterBarlistPaneldetailPanelを直接参照しており、listPanelfilterBarを直接参照している点です。detailPanellistPanelに依存しているため、部品間の依存関係が相互に絡み合うスパゲッティ状態になっています。ある部品の機能変更は他の複数箇所にも影響するため、修正範囲の把握とテストのコストが膨大になります。

After

Mediatorクラスを挟むことで、各部品はMediatorに対してのみイベントを送信する形に変わります。部品同士は直接参照しなくなります。

import java.util.List;

public interface FilterEvent { String getCriteria(); }

public class FilterChangedEvent implements FilterEvent {
    private final String criteria;
    public FilterChangedEvent(String criteria) { this.criteria = criteria; }
    @Override public String getCriteria() { return criteria; }
}

public class FilterBar {
    private Mediator mediator;

    public void setMediator(Mediator m) { this.mediator = m; }

    public String getFilterCriteria() { return "status=OPEN"; }

    public void onFilterChanged(String criteria) {
        mediator.filterChanged(new FilterChangedEvent(criteria));
    }
}

public class TicketListPanel {
    private Mediator mediator;

    public void setMediator(Mediator m) { this.mediator = m; }

    public List<String> getTicketIds() { return List.of("TK-001", "TK-002"); }

    public String getSelectedId() { return "TK-001"; }

    public void onFilterChange(String criteria) {
        System.out.println("[一覧パネル] フィルタ適用: " + criteria);
    }
}

public class TicketDetailPanel {
    private Mediator mediator;

    public void setMediator(Mediator m) { this.mediator = m; }

    public String getTicketDetails() { return "TK-001 details"; }

    public void refresh() {
        System.out.println("[詳細パネル] 再描画しました");
    }
}

public interface Mediator {
    void filterChanged(FilterEvent e);
}

public class TicketCraftMediator implements Mediator {
    private TicketListPanel listPanel;
    private TicketDetailPanel detailPanel;

    public void setComponents(TicketListPanel lp, TicketDetailPanel dp) {
        this.listPanel = lp;
        this.detailPanel = dp;
    }

    @Override
    public void filterChanged(FilterEvent e) {
        listPanel.onFilterChange(e.getCriteria());
        detailPanel.refresh();
    }
}

public class AfterMain {
    public static void main(String[] args) {
        TicketCraftMediator mediator = new TicketCraftMediator();

        FilterBar filterBar = new FilterBar();
        filterBar.setMediator(mediator);

        TicketListPanel listPanel = new TicketListPanel();
        listPanel.setMediator(mediator);

        TicketDetailPanel detailPanel = new TicketDetailPanel();
        detailPanel.setMediator(mediator);

        mediator.setComponents(listPanel, detailPanel);

        filterBar.onFilterChanged(filterBar.getFilterCriteria());
        System.out.println(listPanel.getTicketIds());
        System.out.println(detailPanel.getTicketDetails());
    }
}

実行結果は以下の通りです(Before・After共通)。

[一覧パネル] フィルタ適用: status=OPEN
[詳細パネル] 再描画しました
[TK-001, TK-002]
TK-001 details

各UIクラスは互いを直接参照せず、Mediatorインターフェースのみに依存するようになりました。変更が必要な箇所はMediatorの実装クラスに集約されるため、FilterBarTicketListPanelへの修正が他の部品に影響しなくなります。将来的に通知機能を追加する場合も、既存の3クラスには手を加えずMediatorクラスの拡張だけで対応できます。

🧓 ベテランの現場コラム 部品同士の依存が「直接のメソッド呼び出し」から「仲介者へのイベント送信」に変わると、テスト時にモックへ置き換えやすくなり、ユニットテストが書きやすくなります。ただし仲介者クラス自体が肥大化しやすい点には注意が必要で、責務が増えすぎたら仲介者自体を分割することも検討してください。

こんな場面で使う/使わない

  • 使う: 3つ以上のオブジェクトが互いに複雑にやり取りしている場合。UIとビジネスロジックの連携を整理したい場合
  • 使わない: オブジェクト同士の依存関係が2者だけの場合。すでにイベントバスやDIコンテナで一元管理できている場合

まとめ

  • Mediatorパターンは、オブジェクト間の直接参照を1つの仲介者に置き換えて結合度を下げる
  • イベントベースのやり取りにすることで、コンポーネントの実装変更が他部品に影響しにくくなる
  • 仲介者クラス自体が肥大化しないよう、責務が増えすぎたら分割を検討する

🎓 理解度テスト

学んだ内容を確認しましょう。全3問。