Mediator(仲裁者)模式

本文介绍Mediator模式在软件设计中的应用,通过一个登录对话框示例详细解释如何使用该模式解决对象间复杂通信问题,提高代码可维护性和复用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

16.1 Mediator 模式

  大家想象一下一个乱糟糟的开发小组的工作状态。小组中的 10 个成员虽然一起协同工作,但是意见难以统一,总是互相指挥,导致工作进度始终滞后。他们还都十分在意编码细节,经常为此争执不下。这时,我们就需要一个中立的仲裁者站出来说:“各位,请大家将情况报告给我,我来负责仲裁。我会从团队整体出发进行考虑,然后下达指示。但我不会评价大家的工作细节。” 这样,当出现争执时大家就会找仲裁者进行商量,仲裁者负责统一大家的意见。
  最后,整个团队的交流过程变为了组员向仲裁者报告,仲裁者向组员下达指示。组员之间不再互相询问和相互指示。
  Mediator 的意思是 “仲裁者” “中介者”。一方面,当发生麻烦事情的时候,通知仲裁者;当发生涉及全体组员的事情时,也通知仲裁者。当仲裁者下达指示时,组员会立即执行。团队组员之间不再互相沟通并私自做出决定,而是发生任何事情都向仲裁者报告。另一方面,仲裁者站在整个团队的角度上对组员上报的事情做出决定。这就是 Mediator 模式。
  在 Mediator 模式中, “仲裁者” 被称为 Mediator,各组员被称为 Colleague。Colleague 这个单词很容易拼错,在 GOF 书中就是这么记述的。

16.2 示例程序

  这段 Mediator 模式的示例程序是一个 GUI 应用程序,它展示了一个登录对话框,用户在其中输入正确的用户名和密码后可以登录。示例程序的运行结果如下所示。
运行图
  对话框的使用方法如下:

  • 可以选择作为游客访问(Guest)或是作为用户登录(User)
  • 作为用户登录时,需要输入正确的用户名和密码
  • 点击 OK 按钮可以登录,点击 Cancel 按钮可以取消登录

同时,我们需要根据用户选择的不同以及输入框的状态,来控制按钮是否可以点击。

  • 如果作为游客访问,那么禁用输入框
  • 如果作为用户登录,那么启用输入框
  • 如果用户名输入框没有输入字符,则禁用密码输入框
  • 只有当用户名输入框和密码输入框都至少输入一个字符后, OK 按钮才处于启用状态,可以被按下。用户名输入框或密码输入框中一个字符都没有输入的时候,禁用 OK 按钮
  • Cancel 按钮总是处于启用状态,任何时候都可以按下
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
      如果将上面的逻辑处理分散在各个类中,那么编码的工作量会变得非常大。因为所有的对象都互相关联、互相制约。
      此时,需要调整多个对象的关系时,就需要用到 Mediator 模式了。即不让各个对象之间互相通信,而是增加一个仲裁者角色,让他们各自与仲裁者通信。然后,将控制显示的逻辑处理交给仲裁者负责。
    类和接口一览表:
名字说明
Mediator仲裁者的接口的接口
Colleague组员的接口的接口
ColleagueButton表示按钮的类
ColleagueTextField表示文本框的类
ColleagueCheckbox表示勾选框的类
LoginFrame表示登录对话框的类
Main测试程序行为的类

示例程序类图

示例程序类图
|| Mediator 接口

  Mediator 接口是表示仲裁者的接口。具体的仲裁者(LoginFrame类)会实现这个接口。
  createColleagues 方法用于生成 Mediator 要管理的组员。在示例程序中,会生成对话框中的按钮和文本输入框等控件。
  colleagueChanged 方法会被各个 Colleague 组员调用。它的作用是让组员可以向仲裁者进行报告。在本例中,当单选按钮和文本输入框的状态发生变化时,该方法会被调用。

/**
* 表示仲裁者的接口.
*/
public interface Mediator {

    // 生成要管理的组员方法
    void createColleagues();

    // 组员向仲裁者进行报告方法
    void colleagueChanged();

}
|| Colleague 接口

  Colleague 接口是表示向仲裁者进行报告的组员的接口。具体的组员(本例中是 ColleagueButton、ColleagueTextField、ColleagueCheckbox)会实现这个接口。
  LoginFrame 类实现了 Mediator 接口,它会首先调用 setMediator 方法。该方法的作用是告知组员 “我是仲裁者,有事请报告我”。向该方法中传递的参数是仲裁者的实例,之后在需要向仲裁者报告时会用到该实例。
  setColleagueEnabled 方法的作用是告知组员仲裁者下达的指示。true 表示变为启用状态,反之为禁用状态。并非由组员自己决定,而是由仲裁者来决定。
  此外,在本例中,如果需要让 Mediator 角色和 Colleague 角色之间进行更详细的通信,还需要定义更多的方法。关于 Mediator 和 Colleague 接口中究竟需要定义哪些方法这一点,是根据需求的不同而不同的。也就是说,即使两段程序都使用了 Mediator 模式,但是它们实际定义的方法可能会不同。

/**
* 表示向仲裁者进行报告的组员的接口
*/
public interface Colleague {

    // 设置仲裁者的实例方法
    void setMediator(Mediator mediator);

    // 给组员下达仲裁者命令的方法-这里是控制按钮启用/禁用状态
    void setColleagueEnabled(boolean enabled);
}
|| ColleagueButton 类

  ColleagueButton 类是 java.awt.Button 的子类,它实现了 Colleague 接口,与 LoginFrame (Mediator 接口)共同工作。
  setEnabled 方法,设置禁用/启用组件,传入 true 表示可以被按下,反之,则不可按下。

/**
* 表示按钮的类.
*/
public class ColleagueButton extends Button implements Colleague {

    // 仲裁者实例.
    private Mediator mediator;

    public ColleagueButton(String label) throws HeadlessException {
        super(label);
    }

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

    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnabled(enabled);
    }
}
|| ColleagueTextField 类

  ColleagueTextField 类是 TextField 的子类。它不仅实现了 Colleague 接口,还是实现了 TextListener 接口。这是因为我们希望通过 textValueChanged 方法捕捉到文本内容发生变化这一件事,并通知仲裁者。
  textValueChanged 方法是在 TextListener 接口中定义的方法。当文本内容发生变化时, AWT 框架会调用该方法。在示例程序中,textValueChanged 方法调用了 colleagueChanged 方法。这是在向仲裁者表示 “对不起,文本内容有变化,请处理” 的意思。

/**
* 表示文本输入框的类.
*/
public class ColleagueTextField extends TextField implements TextListener, Colleague {

    private Mediator mediator;

    public ColleagueTextField(String text, int columns) throws HeadlessException {
        super(text, columns);
    }

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

    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnabled(enabled);
        // 设置背景色
        setBackground(enabled ? Color.WHITE : Color.lightGray);
    }

    // 内容发生变化触发
    @Override
    public void textValueChanged(TextEvent e) {
        mediator.colleagueChanged();
    }
}
|| ColleagueCheckbox 类

  ColleagueCheckbox 类是 java.awt.Checkbox 的子类。在示例程序中,我们将其作为单选按钮使用,而没有将其作为勾选框使用(使用 CheckboxGroup)。
  该类实现了 java.awt.event.ItemListener 接口,这是因为我们希望通过 itemSateChanged 方法来捕获单选按钮的状态变化。

/**
 * 勾选框(这里作为单选按钮使用)的类.
*/
public class ColleagueCheckbox extends Checkbox implements ItemListener, Colleague {

    private Mediator mediator;

    public ColleagueCheckbox(String label, CheckboxGroup group, boolean state) throws HeadlessException {
        super(label, group, state);
    }

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

    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnabled(enabled);
    }

    // 状态发生变化触发.
    @Override
    public void itemStateChanged(ItemEvent e) {
        mediator.colleagueChanged();
    }
}
|| LoginFrame 类

  现在我们来看看仲裁者的代码。LoginFrame 类是 java.awt.Frame 的子类,他实现了 Mediator 接口。LoginFrame 类的构造方法进行了以下处理。

  • 设置了背景色
  • 设置布局管理器(配置 4 x 2 窗格)
  • 调用 createColleagues 方法生成 Colleague
  • 配置 Colleague
  • 设置初始状态
  • 显示
      createColleagues 方法会生成登录对话框所需的 Colleague,并将它们保存在 LoginFrame 类的字段中。此外,它还会调用每个 Colleague 的 setMediator 方法,事先告知它们 “我们是仲裁者,有什么问题的可以向我报告”。createColleagues 方法还设置了各个 Colleague 的 Listener。这样 AWT 框架就可以调用合适的 Listener 了。
      colleagueChanged 方法负责前面讲过的 “设置控件的启用/禁用的复杂逻辑处理”。之前的 Colleague 的子类虽然都有设置自身的启用/禁用状态的方法,但是并没有 “具体什么情况下需要设置启用/禁用” 的逻辑处理。它们都只是简单地调用仲裁者的 colleagueChanged 方法告知仲裁者 “剩下的就拜托给你了”。也就是说,所有最终的决定都是由仲裁者的 colleagueChanged 方法下达的。
/**
* 表示登录对话框的类 - 担任仲裁者角色.
*/
public class LoginFrame extends Frame implements ActionListener, Mediator {

    private ColleagueCheckbox checkGuest;
    private ColleagueCheckbox checkLogin;
    private ColleagueTextField textUser;
    private ColleagueTextField textPass;
    private ColleagueButton buttonOk;
    private ColleagueButton buttonCancel;

    public LoginFrame(String title) {
        super(title);
        setBackground(Color.lightGray);
        // 使用 4x2 窗格
        setLayout(new GridLayout(4, 2));
        // 生成各个 Colleague
        createColleagues();
        // 配置
        add(checkGuest);
        add(checkLogin);
        add(new Label("Username:"));
        add(textUser);
        add(new Label("Password:"));
        add(textPass);
        add(buttonOk);
        add(buttonCancel);
        // 设置初始的启用/禁用状态
        colleagueChanged();
        // 显示
        pack();
        setVisible(true);
    }

    @Override
    public void createColleagues() {
        // 生成
        CheckboxGroup g = new CheckboxGroup();
        checkGuest = new ColleagueCheckbox("Guest", g, true);
        checkLogin = new ColleagueCheckbox("Login", g, false);
        textUser = new ColleagueTextField("", 10);
        textPass = new ColleagueTextField("", 10);
        textPass.setEchoChar('*');
        buttonOk = new ColleagueButton("OK");
        buttonCancel = new ColleagueButton("Cancel");

        // 设置仲裁者
        checkGuest.setMediator(this);
        checkLogin.setMediator(this);
        textUser.setMediator(this);
        textPass.setMediator(this);
        buttonOk.setMediator(this);
        buttonCancel.setMediator(this);

        // 设置监听
        checkGuest.addItemListener(checkGuest);
        checkLogin.addItemListener(checkLogin);
        textUser.addTextListener(textUser);
        textPass.addTextListener(textPass);
        buttonOk.addActionListener(this);
        buttonCancel.addActionListener(this);
    }

    // 接收来自于 Colleague 的通知然后判断各个 Colleague 的启用/禁用状态
    @Override
    public void colleagueChanged() {
        // 如果选择游客按钮,则禁止输入用户名、密码
        if (checkGuest.getState()) {
            textUser.setColleagueEnabled(false);
            textPass.setColleagueEnabled(false);
            buttonOk.setColleagueEnabled(true);
        } else {
            textUser.setColleagueEnabled(true);
            userpassChanged();
        }
    }

    // 针对不同的情况,设置 Colleague 的状态
    private void userpassChanged() {
        if (textUser.getText().length() > 0) {
            textPass.setColleagueEnabled(true);
            if (textPass.getText().length() > 0) {
                buttonOk.setColleagueEnabled(true);
            } else {
                buttonOk.setColleagueEnabled(false);
            }
        } else {
            // 未输入 User ,禁止输入密码
            textPass.setColleagueEnabled(false);
            buttonOk.setColleagueEnabled(false);
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println(e.toString());
        System.exit(0);
    }
}
|| Main 类

  Main 类生成了 LoginFrame 类的实例。虽然 Main 类的 main 方法结束了,但是 LoginFrame 类的实例还一直被保存在 AWT 框架中。

public class Main {
    public static void main(String[] args) {
        new LoginFrame("Mediator Sample");
    }
}
16.3 Mediator 模式中的登场角色

  在 Mediator 模式中有以下登场角色。
  ◆ Mediator (仲裁者、中介者)
  Mediator 角色负责定义与 Colleague 角色进行通信和做出决定的接口(API)。在示例程序中,由 Mediator 接口扮演此角色。

  ◆ ConcreteMediator (具体的仲裁者、中介者)
  ConcreteMediator 角色负责实现 Mediator 角色进行通信的接口(API)。在示例程序中,由 LoginFrame 接口扮演此角色。

  ◆ Colleague (同事)
  Colleague 角色负责定义与 Mediator 角色进行通信的接口(API)。在示例程序中,由 Colleague 接口扮演此角色。

  ◆ ConcreteColleague (具体的同事)
  ConcreteColleague 角色负责实现 Colleauge 角色的接口(API)。在示例程序中,由 ColleagueButton类、ColleagueText类和 ColleagueCheckbox 类扮演此角色。
Mediator 模式 UML 类图

Mediator 模式 UML 类图
16.4 拓展思路的要点
|| 当发生分散灾难时

  在示例程序中 LoginFrame 类的 colleagueChanged 方法稍微有些复杂。如果发生需求变更,该方法中很容易产生 Bug。不过这并不是什么问题。因为即使 colleagueChanged 方法中发生了 Bug,由于其他地方并没有控制控件的启用/禁用状态的逻辑处理,因此只需要调试该方法就很容易地找出 Bug 的原因。
  如果这段逻辑分布在 Colleague 的各子类中,那么无论是编写代码还是调试代码和修改代码,都会非常困难。
  通常情况下,面向对象编程可以帮助我们分散处理,避免处理过于集中,也就是说可以 “分而治之”,但是在本章中的示例程序中,把处理分散在各个类中是不明智的。如果只是将应当分散的处理分散在各个类中,但是没有将应当集中的处理集中起来,那么分散的类最终只会导致灾难。

|| 通信线路的增加

  假设现在有 A 和 B 这 2 个实例,它们之间互相通信(相互之间调用方法),那么通信线路有两条,即 A -> B 和 A <- B。如果有 A 、B 和 C 这 3 个实例,那么就会有 6 条通信线路。以此类推,4条就会有12条同线路;5个实例就是20条通信线路。呈指数级上升。程序的结构会变得非常复杂。
  可能会认为,如果实例很少就不需要 Mediator 模式了。但是考虑到的是,即使最初实例很少,很可能随着需求的变更实例数量会慢慢变多,迟早会暴露出问题。

|| 哪些角色可以复用

  ConcreteColleague 角色可以复用,但 ConcreteMediator 角色很难复用。
  这是因为 ConcreteColleague 角色中并没有任何依赖于特定对话框的代码。在示例程序中,依赖于特定应用程序的部分被封装在扮演 ConcreteMediator 角色的 LoginFrame 中。依赖于特定应用程序就意味着难以复用。因此, LoginFrame 类很难在其他对话框中被复用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值