広告

Java : Mediator パターン (図解/デザインパターン)

Mediator パターンとは、GoF によって定義されたデザインパターンの1つです。
相互に依存する複数のクラスがある場合に、その依存関係をシンプルにする、という目的のための設計です。

本記事では、Mediator パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

概要

Mediator パターン は、ソフトウェアのデザインパターンの一つで、統一されたインタフェース の集合を提供するパターンである。

Mediator パターンとは、

  • 相互に依存する複数のクラスがある場合に
  • 仲介者をはさむことで、その依存関係をシンプルにする

という設計です。

Mediator は、日本語的に発音すると「メディエーター」となります。
意味は「仲介者」ですね。


【 Mediator パターンのイメージ図 】

ユースケース図1

クラスA、クラスB、クラスC、クラスD は互いに依存しています。
つまり、それぞれのクラスは、他の 3つの クラスに依存していることになります。


ユースケース図2

Mediator パターンでは、仲介者を介することで依存関係をシンプルにします。

クラスA、クラスB、クラスC、クラスD は、仲介者とだけ依存します。
つまり、それぞれのクラスは 1つの 依存だけになります。

依存関係が減れば、それだけそのクラスはシンプルになるでしょう。
自分の役割にも専念できます。

ただし、仲介者は4つの依存(クラスA, B, C, D) を持つことになります。
そのため、Mediator パターンの仲介者は 役割が多く なりがちです。そこは注意したいですね。

仲介者とその他のクラスのやりとりには、少し簡略化した Observer パターン を使います。
よって Mediator パターンは、Observer パターンを応用したもの、と言えるかもしれません。

もし余裕があれば、先に Observer パターン の記事をご確認いただくことをおすすめします。

関連記事:


クラス図とシーケンス図

クラス図1

シーケンス図1

上の図は、Mediator パターンの一般的なクラス図とシーケンス図です。

ConcreteMediator が 仲介 して

  • ConcreteColleagueA
  • ConcreteColleagueB

2つのクラスを操作 するのがポイントです。
ConcreteColleagueA と ConcreteColleagueB はお互いを知りません。(依存しない)

このクラス図をそのままコードにしてみましょう。

public interface Mediator {
    void notify(Colleague colleague);
}

public class ConcreteMediator implements Mediator {
    private ConcreteColleagueA colleagueA;
    private ConcreteColleagueB colleagueB;

    public void setColleague(ConcreteColleagueA colleagueA, ConcreteColleagueB colleagueB) {
        this.colleagueA = colleagueA;
        this.colleagueB = colleagueB;
    }

    @Override
    public void notify(Colleague colleague) {
        if (colleague == colleagueA) {
            final var a = colleagueA.getStateA();
            colleagueB.actionB(a);
        } else if (colleague == colleagueB) {
            final var b = colleagueB.getStateB();
            colleagueA.actionA(b);
        }
    }
}
public class Colleague {
    protected final Mediator mediator;

    public Colleague(Mediator mediator) {
        this.mediator = mediator;
    }
}

public class ConcreteColleagueA extends Colleague {

    public ConcreteColleagueA(Mediator mediator) {
        super(mediator);
    }

    public void operate() {
        mediator.notify(this);
    }

    public String getStateA() {
        return "State A!";
    }

    public void actionA(String value) {
        System.out.println("Action A : 相手の状態は ... " + value);
    }
}

public class ConcreteColleagueB extends Colleague {

    public ConcreteColleagueB(Mediator mediator) {
        super(mediator);
    }

    public void operate() {
        mediator.notify(this);
    }

    public String getStateB() {
        return "State B!";
    }

    public void actionB(String value) {
        System.out.println("Action B : 相手の状態は ... " + value);
    }
}

次に ConcreteMediator と ConcreteColleague を使う例です。

final var mediator = new ConcreteMediator();

final var colleagueA = new ConcreteColleagueA(mediator);
final var colleagueB = new ConcreteColleagueB(mediator);

mediator.setColleague(colleagueA, colleagueB);

colleagueA.operate();
colleagueB.operate();

// 結果
// ↓
//Action B : 相手の状態は ... State A!
//Action A : 相手の状態は ... State B!

具体的な例

もう少し具体的な例も見てみましょう。

とてもシンプルな名前の設定画面を考えてみます。

UI図1

名前の設定画面では、名前入力欄に…

  • 文字列が入力されたら OKボタンは 有効
  • 空なら OKボタンは 無効

になるようにします。

とはいえ、GUI をコード例にするのは複雑になるので、ここでは単純なテキスト出力のみで考えます。
(あくまで名前の設定画面はイメージということで…)

Mediator パターンを使わないケース

まずは Mediator パターンを使わないケースを考えてみます。

登場するクラスは次のようになります。

  • 名前設定画面 : View クラス
  • 名前入力欄 : TextField クラス
  • OKボタン : Button クラス

クラス図2

TextField クラスは Button クラスを持ちます。(依存)
そして、文字列を設定(setValue) するときに、文字列が空かどうかで OKボタンの有効/無効を切り替えます。

public class TextField {
    private String value = "";
    private Button button;

    public void setButton(Button button) {
        this.button = button;
    }

    public void setValue(String value) {
        this.value = value;

        // ★ 文字列が空かどうかで、ボタンの有効/無効を切り替えます。
        button.setEnabled(!value.isEmpty());
    }

    public String getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "名前入力欄 : " + value;
    }
}

Button クラスは TextField クラスを持ちます。(依存)
setEnabled メソッドでは、ボタンの有効/無効を設定します。

そして、ボタンが押されたら(action)、TextFiled から名前を取得して表示します。

public class Button {
    private boolean enabled;
    private TextField textField;

    public void setTextField(TextField textField) {
        this.textField = textField;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public void action() {
        if (!enabled) {
            return;
        }

        // ★ ボタンが押されたら名前を取得して表示します。
        final var name = textField.getValue();
        System.out.println("名前を " + name + " に設定しました!");
    }

    @Override
    public String toString() {
        if (enabled) {
            return "OKボタン : 有効";
        } else {
            return "OKボタン : 無効";
        }
    }
}

View クラスは toString メソッドで画面情報を作成するだけです。

public class View {
    private TextField textField;
    private Button button;

    public void setComponent(TextField textField, Button button) {
        this.textField = textField;
        this.button = button;
    }

    @Override
    public String toString() {
        return """
                ---- 名前設定画面 ----
                %s
                %s
                --------
                """.formatted(textField, button);
    }
}

それでは、これらのクラスを使ってみましょう。

final var textField = new TextField();
final var button = new Button();

textField.setButton(button);
button.setTextField(textField);

final var view = new View();
view.setComponent(textField, button);

// 初期状態
System.out.println(view);

// 結果
// ↓
//---- 名前設定画面 ----
//名前入力欄 :
//OKボタン : 無効
//--------

// 名前を入力
textField.setValue("アリス");
System.out.println(view);

// 結果
// ↓
//---- 名前設定画面 ----
//名前入力欄 : アリス
//OKボタン : 有効
//--------

// OKボタンを押す
button.action();

// 結果
// ↓
//名前を アリス に設定しました!

問題なく動作しました。

今回の例はわりとシンプルなので、あまり問題に感じないかもしれません。
しかし、入力欄やボタンがさらに増えると、とたんに複雑になります。

また、TextFiled と Button クラスは互いに依存しているので、あまり汎用的なクラスとは言えません。

例えば、苗字と名前で2つの入力欄を持つ画面を考えてみましょう。
苗字と名前の両方が入力されたら OKボタンを有効にします。

UI図2

今までの考え方で依存関係を図にすると、次のようになります。

クラス図3

View クラスの修正は仕方ないですが、TextField と Button クラスも修正が必要です。
例えば TextField では、苗字用と名前用の2つの TextField が連携して、OKボタンの有効/無効を切り替えます。

public class TextField {
    private TextField otherTextField;
    private Button button;

    ... 省略 ...

    public void setValue(String value) {
        // ★ 自身と、もう片方の入力欄のどちらかが空だったら
        //    ボタンを無効にします。
        final var empty = value.isEmpty()
                || otherTextField.getVallue().isEmpty();
        button.setEnabled(!empty);
    }
}

OKボタンも、苗字と名前の両方の TextField から文字列を取得する必要があります。

もし Observer パターン をご存じのかたは、もう少しシンプルな依存関係になると思います。
そして、Mediator パターンとは Observer パターンを応用したものです。


Mediator パターンを使うケース

次は、Mediator パターン を使う例を見てみましょう。

先ほど Mediator パターンを使わないケース で作成した

  • TextField
  • Button

の2つのクラスは、Mediator パターンの ConcreteColleague に相当します。
View クラスは ConcreteMediator に相当します。

クラス図4

まずは Mediator インタフェースです。

notify メソッドでは Component をパラメータにします。
これにより、TextField と Button どちらの通知なのか判断できます。

public interface Mediator {
    void notify(Component component);
}

(今回、notify メソッドのパラメータは Component だけですが、補助的なパラメータを追加してもよいと思います)

次に View クラスです。
Mediator インタフェースを実装(implements) します。

public class View implements Mediator {
    private TextField textField;
    private Button button;

    public void setComponent(TextField textField, Button button) {
        this.textField = textField;
        this.button = button;
    }

    @Override
    public void notify(Component component) {
        if (component == textField) {

            // ★ 文字列が空かどうかで、ボタンの有効/無効を切り替えます
            final var name = textField.getValue();
            button.setEnabled(!name.isEmpty());

        } else if (component == button) {

            // ★ ボタンが押されたら名前を取得して表示します。
            final var name = textField.getValue();
            System.out.println("名前を " + name + " に設定しました!");
        }
    }

    @Override
    public String toString() {
        return """
                ---- 名前設定画面 ----
                %s
                %s
                --------
                """.formatted(textField, button);
    }
}

notify メソッドの中身がポイントですね。
TextField と Button の 連携 が必要な処理をします。

Component クラスは、Mediator インタフェースを保持します。

public class Component {
    protected final Mediator mediator;

    public Component(Mediator mediator) {
        this.mediator = mediator;
    }
}

最後に Component クラスのサブクラスとなる

  • TextField クラス
  • Button クラス

です。

public class TextField extends Component {
    private String value = "";

    public TextField(Mediator mediator) {
        super(mediator);
    }

    public void setValue(String value) {
        this.value = value;

        // ★ ポイント
        mediator.notify(this);
    }

    public String getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "名前入力欄 : " + value;
    }
}

public class Button extends Component {
    private boolean enabled;

    public Button(Mediator mediator) {
        super(mediator);
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public void action() {
        if (!enabled) {
            return;
        }

        // ★ ポイント
        mediator.notify(this);
    }

    @Override
    public String toString() {
        if (enabled) {
            return "OKボタン : 有効";
        } else {
            return "OKボタン : 無効";
        }
    }
}

TextField と Button クラスは、互いに 依存していない ことが重要です。
Mediator だけを意識すれば OK です。

それでは、Mediator パターンに対応した View クラスを使ってみましょう。

final var view = new View();

final var textField = new TextField(view);
final var button = new Button(view);

view.setComponent(textField, button);

// 初期状態
System.out.println(view);

// 結果
// ↓
//---- 名前設定画面 ----
//名前入力欄 :
//OKボタン : 無効
//--------

// 名前を入力
textField.setValue("アリス");
System.out.println(view);

// 結果
// ↓
//---- 名前設定画面 ----
//名前入力欄 : アリス
//OKボタン : 有効
//--------

// OKボタンを押す
button.action();

// 結果
// ↓
//名前を アリス に設定しました!

結果も問題なしですね。

依存関係がシンプルになったので、仕様の変更にも強くなります。
例えば、名前入力欄が苗字と名前の2つになったとしても、View だけの修正ですみます。

UI図2

public class View implements Mediator {
    private TextField firstNameTextField;
    private TextField lastNameTextField;
    private Button button;

    ... 省略 ...

    @Override
    public void notify(Component component) {
        if (component == firstNameTextField || component == lastNameTextField) {

            // ★ 入力欄のどちらかが空だったらボタンを無効にします。
            final var empty = firstNameTextField.getValue().isEmpty()
                    || lastNameTextField.getValue().isEmpty();
            button.setEnabled(!empty);

        } else if (component == button) {

            // ★ ボタンが押されたら苗字と名前を取得して表示します。
            final var firstName = firstNameTextField.getValue();
            final var lastName = lastNameTextField.getValue();
            System.out.println("名前を " + firstName + lastName + " に設定しました!");
        }
    }

    ... 省略 ...
}

TextField と Button クラスの修正は必要ありません。
Mediator パターンで依存関係がシンプルになった恩恵ですね。

まとめ

Mediator パターンとは、

  • 相互に依存する複数のクラスがある場合に
  • 仲介者をはさむことで、その依存関係をシンプルにする

という設計です。

仲介者とその他のクラスのやりとりには、少し簡略化した Observer パターン を使います。
よって Mediator パターンは、Observer パターンを応用したもの、と言えるかもしれません。

依存関係が複雑になってしまった場合は、Mediator パターンを検討してみましょう。


関連記事

ページの先頭へ