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 类扮演此角色。
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 类很难在其他对话框中被复用。