概要

複数のオブジェクトが互いに直接参照し合うと、依存関係が網目状に複雑化します。Mediator パターンは、オブジェクト間の通信をすべて仲介者(Mediator)に集約し、各オブジェクト(Colleague)は Mediator だけを知っていればよい設計にします。チャットルーム、フォーム入力の連動制御、イベントバスなどが典型的な適用場面です。この記事では、チャットルームを題材に、一般ユーザーと管理者ユーザーのメッセージ配信を Mediator で仲介する実装を示します。record によるメッセージの不変化、List.copyOf による防御的コピー、Java 21 の sealed interface でユーザーロールを型安全に表現する方法も確認します。

使いどころ

チャットルームで送信者以外の全参加者にメッセージを配信する仲介オブジェクトを実装する

画面フォームのコンポーネント連動(例: 都道府県セレクトが変わったら市区町村セレクトの選択肢を更新)を Mediator に集約する

マイクロサービス間のイベント通知をイベントバス(Mediator)経由に統一し、サービス同士の直接依存を排除する

コード例

MediatorPatternDemo.java
import java.util.ArrayList;
import java.util.List;

public class MediatorPatternDemo {

    record Message(String content, String senderName) {
        @Override public String toString() {
            return "[" + senderName + "]: " + content;
        }
    }

    interface ChatMediator {
        void sendMessage(Message message, ChatUser sender);
        void addUser(ChatUser user);
        List<ChatUser> getUsers();
    }

    static abstract class ChatUser {
        protected final ChatMediator mediator;
        protected final String name;
        ChatUser(ChatMediator mediator, String name) {
            this.mediator = mediator; this.name = name;
        }
        abstract void send(String content);
        abstract void receive(Message message);
    }

    static class ChatRoom implements ChatMediator {
        private final List<ChatUser> users = new ArrayList<>();
        @Override public void addUser(ChatUser user) {
            users.add(user);
            System.out.println(user.name + " が参加");
        }
        @Override
        public void sendMessage(Message msg, ChatUser sender) {
            for (var user : users) {
                if (user != sender) user.receive(msg);
            }
        }
        @Override public List<ChatUser> getUsers() {
            return List.copyOf(users);
        }
    }

    static class GeneralUser extends ChatUser {
        GeneralUser(ChatMediator m, String name) {
            super(m, name);
        }
        @Override public void send(String content) {
            var msg = new Message(content, name);
            System.out.println("[送信] " + msg);
            mediator.sendMessage(msg, this);
        }
        @Override public void receive(Message msg) {
            System.out.println("[受信] " + name + " <- " + msg);
        }
    }

    static class AdminUser extends ChatUser {
        AdminUser(ChatMediator m, String name) {
            super(m, name);
        }
        @Override public void send(String content) {
            var msg = new Message("[管理者通知] " + content, name);
            System.out.println("[管理者送信] " + msg);
            mediator.sendMessage(msg, this);
        }
        @Override public void receive(Message msg) {
            System.out.println("[管理者受信] " + name
                             + " <- " + msg);
        }
    }

    public static void main(String[] args) {
        var room = new ChatRoom();
        var alice = new GeneralUser(room, "Alice");
        var bob = new GeneralUser(room, "Bob");
        var admin = new AdminUser(room, "Admin");

        room.addUser(alice);
        room.addUser(bob);
        room.addUser(admin);

        alice.send("こんにちは!");
        admin.send("メンテナンスのお知らせです");
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record でメッセージを不変オブジェクト化し、List.copyOf で簡潔に防御的コピーを返せる。var も利用可。

Java 17
// Java 17: record で不変なメッセージオブジェクト
record Message(String content, String senderName) {}

public void sendMessage(Message message, ChatUser sender) {
    for (var user : users) {
        if (user != sender) {
            user.receive(message);
        }
    }
}
public List<ChatUser> getUsers() {
    return List.copyOf(users); // 防御的コピー
}

Library Comparison

標準 API(interface + List)参加者が少なくメッセージ配信が単純な場面。外部依存なしで完結する。非同期配信やメッセージフィルタリングが必要になると自前コードが増える。
Guava EventBusイベント駆動でコンポーネント間の通信を疎結合にしたい場合。アノテーションベースで購読できる。Guava への依存が増える。非推奨との見方もあり、新規では CDI Event 等を検討。
Spring ApplicationEventSpring アプリケーション内のコンポーネント間イベント通知。DI コンテナと統合されている。Spring 依存になるため、ライブラリ単体やバッチでは使いにくい。

注意点

Mediator 自体が肥大化しやすい。ロジックが増えてきたら Mediator を分割するか、イベント駆動アーキテクチャを検討する

Colleague が Mediator 以外の Colleague を直接呼び出すと、パターンの意味がなくなる。コードレビューで直接参照がないか確認する

getUsers() で内部リストをそのまま返すと外部から変更される恐れがある。List.copyOf で防御的コピーを返すこと

Mediator と Observer は似ているが、Observer は1対多の通知が基本。Mediator は多対多の双方向通信の整理が目的。使い分けを意識する

FAQ

Mediator パターンと Observer パターンの違いは何ですか。

Observer は1つの Subject が複数の Observer に一方向に通知します。Mediator は複数の Colleague 間の双方向通信を仲介し、通信ロジックを Mediator に集約します。

Mediator が肥大化したらどうすればよいですか。

メッセージの種類やユーザーロールごとに Mediator を分割するか、各処理をハンドラークラスに委譲して Mediator 自体は振り分けだけに絞ります。

GUI フレームワークでも Mediator は使われていますか。

はい。Swing の JDialog がコンポーネント間の連動を仲介するのは Mediator の一種です。現代の MVVM や Redux も広義には Mediator の役割を担っています。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます