中介者模式

设计模式笔记:中介者模式 (Mediator Pattern)

一、一句话概括

用一个中介对象来封装一系列对象之间的复杂交互,使得这些对象不再需要显式地相互引用,从而降低它们之间的耦合度。它将网状的通信结构转变为星形的通信结构

二、为什么需要它 (Why - The Pain Point)

想象一个复杂的 GUI 对话框,上面有多个控件:一个文本框(TextBox)、一个按钮(Button)、一个列表框(ListBox)、一个复选框(Checkbox)。它们之间存在复杂的交互逻辑:

  • 当文本框内容为空时,按钮应被禁用。
  • 当在列表框中选择一项时,该项的内容应显示在文本框中。
  • 当复选框被选中时,列表框中的所有项都应被清除。
  • … 等等

没有中介者模式的糟糕设计:

每个控件都需要直接持有其他相关控件的引用,并直接调用它们的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 痛点:每个组件都需要知道其他组件的存在和接口
class TextBox {
private Button button;
private ListBox listBox;

public void onTextChanged() {
if (this.getText().isEmpty()) {
button.setEnabled(false);
} else {
button.setEnabled(true);
}
}
}

class ListBox {
private TextBox textBox;
private Checkbox checkbox;

public void onSelectionChanged() {
textBox.setText(this.getSelected());
}
}
// ... Button 和 Checkbox 同样复杂

痛点:蜘蛛网式的耦合 (Spiderweb Coupling)

  1. 高度耦合:每个组件都与其他多个组件紧密耦合,形成一个难以理解和维护的“蜘蛛网”。
  2. 难以复用TextBox 组件因为它依赖于特定的 ButtonListBox,所以很难将它单独拿到另一个对话框中复用。
  3. 违反开闭原则:如果增加一个新的控件,比如一个滑动条 Slider,并且它需要与 TextBoxButton 交互,那么你可能需要修改 TextBoxButton 的代码来添加对 Slider 的引用和处理逻辑。

核心问题:如何管理一组对象之间复杂的、多对多的交互关系,从而避免它们之间产生紧密的网状耦合?

三、它是什么 (What - The Solution)

中介者模式通过引入一个中心化的中介者(Mediator) 对象来解决这个问题。所有的“同事”对象(Colleague)不再直接通信,而是将消息发送给中介者,由中介者来协调和转发。

核心思想和角色:

  1. **中介者接口 (Mediator)**:
    • 定义了同事对象与中介者对象之间通信的接口。通常包含一个方法,如 notify(Component sender, String event),用于接收同事对象发来的通知。
  2. **具体中介者 (Concrete Mediator)**:
    • 实现了中介者接口。
    • 了解并持有所有同事对象的引用
    • 它封装了对象之间复杂的交互逻辑。当它从一个同事对象接收到消息后,它会根据预设的逻辑去调用其他一个或多个同事对象的方法。
  3. **同事接口/抽象类 (Colleague)**:
    • 定义了同事对象的通用接口。
    • 持有一个中介者对象的引用
  4. **具体同事 (Concrete Colleague)**:
    • 实现了同事接口。
    • 当自身状态发生改变,需要通知其他同事时,它不会直接调用其他同事,而是调用中介者的 notify() 方法,将自己作为发送者传进去。
    • 它也提供了供中介者调用的方法,以改变自身状态。

工作流程:
ListBox 中有选项被选中时:

  1. ListBox 调用 mediator.notify(this, "selectionChanged")
  2. ConcreteMediator 接收到通知,知道是 ListBox 发来的 selectionChanged 事件。
  3. 根据内部逻辑,ConcreteMediator 调用 textBox.setText(...)

现在,ListBox 只认识 Mediator,完全不知道 TextBox 的存在。

四、怎么做 (How - The Blueprint)

基本步骤(以 GUI 对话框为例):

  1. 创建同事类Button, TextBox 等),每个同事类都持有一个 Mediator 的引用。
  2. **创建中介者接口 Mediator**,定义一个 notify 方法。
  3. **创建具体中介者 DialogMediator**,实现 Mediator 接口。
    • 它持有所有同事对象(Button, TextBox 等)的引用。
    • notify 方法中实现复杂的协调逻辑。
  4. 在客户端代码中,创建所有同事对象和中介者对象,并将中介者注入到每个同事中,同时将同事注册到中介者中。

Java 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// --- 1. 中介者接口 (Mediator) ---
interface Mediator {
void notify(Component sender, String event);
}

// --- 2. 同事抽象 (Colleague) ---
abstract class Component {
protected Mediator mediator;

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

public void click() {
mediator.notify(this, "click");
}

public void keypress() {
mediator.notify(this, "keypress");
}
}

// --- 3. 具体同事 (Concrete Colleague) ---
class Button extends Component {
private boolean enabled;
public Button(Mediator mediator) { super(mediator); }
public void setEnabled(boolean enabled) {
this.enabled = enabled;
System.out.println("Button is now " + (enabled ? "enabled" : "disabled"));
}
}

class TextBox extends Component {
private String content = "";
public TextBox(Mediator mediator) { super(mediator); }
public void setContent(String content) { this.content = content; }
public String getContent() { return content; }
}

class ListBox extends Component {
private String selection = "";
public ListBox(Mediator mediator) { super(mediator); }
public void setSelection(String selection) {
this.selection = selection;
System.out.println("ListBox selected: " + selection);
// 状态改变,通知中介者
this.click();
}
}


// --- 4. 具体中介者 (Concrete Mediator) ---
class AuthenticationDialog implements Mediator {
// 中介者持有所有同事的引用
private TextBox usernameBox;
private TextBox passwordBox;
private Button loginButton;
private ListBox userType;

// 提供 setter 来注册同事
public void setUsernameBox(TextBox usernameBox) { this.usernameBox = usernameBox; }
public void setPasswordBox(TextBox passwordBox) { this.passwordBox = passwordBox; }
public void setLoginButton(Button loginButton) { this.loginButton = loginButton; }
public void setUserType(ListBox userType) { this.userType = userType; }

// 核心的协调逻辑
@Override
public void notify(Component sender, String event) {
System.out.println("Mediator received notification from " + sender.getClass().getSimpleName() + " with event " + event);
if (sender == usernameBox || sender == passwordBox) {
// 当用户名或密码框内容改变时
if (!usernameBox.getContent().isEmpty() && !passwordBox.getContent().isEmpty()) {
loginButton.setEnabled(true);
} else {
loginButton.setEnabled(false);
}
} else if (sender == userType && "click".equals(event)) {
// 当用户类型选择改变时
if ("Admin".equals(userType.selection)) {
System.out.println("Admin selected, showing special fields (logic in mediator)...");
}
}
}
}

// --- 5. 客户端 ---
public class MediatorPatternDemo {
public static void main(String[] args) {
AuthenticationDialog mediator = new AuthenticationDialog();

// 创建同事,并注入中介者
TextBox username = new TextBox(mediator);
TextBox password = new TextBox(mediator);
Button login = new Button(mediator);
ListBox type = new ListBox(mediator);

// 在中介者中注册同事
mediator.setUsernameBox(username);
mediator.setPasswordBox(password);
mediator.setLoginButton(login);
mediator.setUserType(type);

System.out.println("--- Initial State ---");
username.keypress(); // 模拟输入,触发检查

System.out.println("\n--- User fills in details ---");
username.setContent("admin");
password.setContent("12345");
username.keypress(); // 再次触发检查

System.out.println("\n--- User selects a type ---");
type.setSelection("Admin");
}
}

五、UML 类图

下面是上述例子的 PlantUML 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@startuml
skinparam classAttributeIconSize 0
hide empty members

interface Mediator {
+ void notify(Component sender, String event)
}

abstract class Component {
# Mediator mediator
+ Component(Mediator m)
}

class Button extends Component
class TextBox extends Component
class ListBox extends Component

class AuthenticationDialog implements Mediator {
- TextBox usernameBox
- Button loginButton
...
+ void notify(Component sender, String event)
}

' Relationships
' Colleagues HAVE-A Mediator
Component o-- "1" Mediator

' Mediator HAS-A list of Colleagues
AuthenticationDialog *-- "1..*" Component

' Client uses both
class MediatorPatternDemo {}
MediatorPatternDemo ..> AuthenticationDialog
MediatorPatternDemo ..> Component
@enduml

图示解读:

  • 所有 Component(同事)都与 Mediator(中介者)关联。
  • AuthenticationDialog(具体中介者)反过来也与所有 Component 关联。
  • 形成了一个清晰的星形结构,Mediator 位于中心。

六、优缺点分析

优点 (Pros):

  1. 降低耦合度:将网状的多对多关系转变为星形的点对多关系,同事类之间不再直接依赖,只依赖于中介者,极大地降低了系统的耦合性。
  2. 提高可复用性:同事类因为不再依赖其他同事,变得更容易被复用。
  3. 集中控制逻辑:复杂的交互逻辑被封装在中介者中,使得逻辑更集中,易于理解和维护。
  4. 符合开闭原则:增加新的同事类通常不需要修改其他同事类,只需要修改中介者即可。

缺点 (Cons):

  1. 中介者可能变得过于复杂:将所有交互逻辑都集中到一个中介者中,可能导致中介者类自身变得非常庞大和复杂,成为一个难以维护的“上帝对象”(God Object)。

七、在 Java 中的应用

  • java.util.Timer: Timer 类可以被看作一个中介者。你可以安排多个 TimerTask(同事)在指定时间执行。Timer 负责管理所有任务的调度,而各个 TimerTask 之间互不关心。
  • GUI 框架: 各种 GUI 框架的事件处理机制,如 Java Swing 中的 ActionListener,虽然不完全是经典的中介者模式,但思想相通。通常会有一个控制器(Controller)或表现器(Presenter)对象充当中介者,来协调视图(View)中各个组件的交互。
  • Executor 框架: java.util.concurrent.Executor 接口及其实现(如 ThreadPoolExecutor)也扮演了中介者的角色。你提交 RunnableCallable 任务(同事)给它,它负责协调线程池中的线程(另一组同事)来执行这些任务。任务与线程之间完全解耦。