Java 设计模式心法之第25篇 - 中介者 (Mediator) - 用“中央协调”降低对象间耦合度

在复杂的系统中,多个对象之间常常需要相互协作来完成任务。如果让这些对象直接相互引用和通信 ,很容易形成一个错综复杂的"网状"依赖关系 。在这种结构下,任何一个对象的改变都可能波及到其他多个对象,导致系统耦合度极高 ,难以理解、维护和扩展。想象一下一个繁忙的机场,如果每架飞机都需要直接与其他所有飞机、塔台、地勤、登机口协调,那将是怎样一场混乱?本文将带你深入理解行为型模式中的"交通管制塔"------中介者模式。我们将揭示它如何引入一个中心中介者 (Mediator) 对象来封装 一系列对象之间的交互 ,使得各个对象(称为同事 Colleague )不再直接相互通信,而是都通过中介者 来协调。这样,网状的依赖关系就变成了星型 的依赖关系(每个同事只依赖中介者),从而显著降低了对象间的耦合度,提高了系统的可维护性和灵活性。


一、问题的提出:当"自由沟通"遭遇"网状混乱"

想象一下我们正在设计一个简单的 GUI 对话框,包含以下组件:

  • 文本框 (TextBox): 输入用户名。
  • 复选框 (CheckBox): 是否同意协议。
  • 按钮 (Button): "注册"按钮。

这些组件之间存在一些交互逻辑:

  1. 当文本框不为空 且复选框被选中 时,"注册"按钮才可用
  2. 当文本框为空 或复选框未被选中 时,"注册"按钮应禁用
  3. 文本框内容改变时,需要检查条件并可能更新按钮状态。
  4. 复选框状态改变时,也需要检查条件并可能更新按钮状态。

如果我们让这些组件直接相互引用和通信

  • 文本框需要持有按钮的引用,以便在内容改变时通知按钮更新状态。
  • 复选框也需要持有按钮的引用,以便在状态改变时通知按钮更新状态。
  • 按钮可能需要持有文本框和复选框的引用,以便在某些情况下(虽然在这个简单例子里不太需要)获取它们的状态。

现在,如果再增加一个"确认密码"文本框,并且注册按钮还需要在两个密码框内容一致时才可用,那么:

  • 两个密码框都需要持有按钮的引用。
  • 按钮可能需要持有两个密码框的引用。
  • 每个密码框内容改变时,都需要通知按钮(可能还需要通知另一个密码框进行比对)。

问题显而易见:

  1. 网状依赖 (Mesh of Dependencies): 对象之间形成了复杂的、多对多的依赖关系。每个对象都需要知道其他多个与之交互的对象。
  2. 高耦合 (High Coupling): 任何一个组件的修改(比如按钮的更新逻辑改变)都可能影响到所有依赖它的其他组件。增加或移除一个组件,也需要修改所有与之相关的组件的代码。
  3. 逻辑分散 (Scattered Logic): 组件间交互的协调逻辑(如何根据文本框和复选框状态更新按钮)分散在各个组件(文本框、复选框)的事件处理代码中,难以集中管理和理解。
  4. 难以复用 (Difficult Reuse): 单个组件(如文本框)因为与其他特定组件紧密耦合,很难被独立地复用到其他对话框或场景中。

我们需要一种机制,能够打破这种网状依赖 ,将复杂的对象间交互逻辑集中管理,让对象之间不再直接"对话",而是通过一个"中心枢纽"来传递消息和协调行为。

二、中心协调的智慧:中介者模式的核心定义与意图

中介者模式 (Mediator Pattern) 提供了一种解决方案。它用一个中介对象 (Mediator)封装一系列的对象交互 。中介者使各对象不需要显式地相互引用 ,从而使其耦合松散 ,而且可以独立地改变它们之间的交互

GoF 的经典意图描述是:"用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。"

其核心思想在于引入中介者作为通信的中心:

  1. 定义同事接口/抽象类 (Colleague Interface/Abstract Class): 定义一个接口或抽象类,让所有需要通过中介者进行通信的对象(称为同事 Colleague)都实现或继承它。这个接口通常包含一个指向中介者对象的引用,以及一个用于接收中介者通知的方法(或者同事通过中介者直接发送消息)。
  2. 定义中介者接口/抽象类 (Mediator Interface/Abstract Class): 定义一个接口,包含用于同事对象注册 以及转发同事请求的方法。它声明了中介者需要提供的协调服务。
  3. 具体同事类实现 (Concrete Colleague Implementation): 实现 Colleague 接口/继承抽象类。
    • 持有中介者引用: 每个具体同事对象都持有一个指向中介者对象的引用。
    • 通过中介者通信: 当一个同事对象需要与其他同事交互时,它不直接 调用其他同事的方法,而是通知中介者(调用中介者的方法)。
    • 响应中介者通知: 它也需要实现接收中介者通知的方法,以便在其他同事通过中介者影响到它时做出响应。
  4. 具体中介者实现 (Concrete Mediator Implementation): 实现 Mediator 接口。
    • 了解并维护所有同事: 具体中介者需要知道它所协调的所有具体同事对象(通常通过注册机制持有它们的引用)。
    • 封装交互逻辑:集中实现了 原本分散在各个同事对象中的复杂交互逻辑。当收到某个同事的通知时,中介者根据预定义的协调规则,决定需要通知哪些其他的同事对象执行相应的操作。

核心角色:

  • Mediator (中介者接口/抽象类): 定义了同事对象到中介者对象的接口,用于注册同事和转发请求。
  • ConcreteMediator (具体中介者): 实现 Mediator 接口,协调各个同事对象之间的交互。它必须了解并维护所有的同事。
  • Colleague (同事接口/抽象类): 定义了中介者对象到同事对象的接口,通常包含一个指向 Mediator 的引用。
  • ConcreteColleague (具体同事): 实现 Colleague 接口,是系统中实际进行交互的对象。每个同事只知道自己的中介者,不知道其他同事。当需要通信时,它通知中介者。

关键:将网状交互变为星型交互,所有通信通过中介者进行,交互逻辑集中在中介者内部。

三、解耦通信的场景:中介者模式的适用之地

中介者模式在以下需要简化对象间复杂交互、降低耦合度的场景中非常有用:

  • 一组对象以定义良好但复杂的方式进行通信: 对象之间存在多对多的依赖关系,导致系统难以理解和维护。
  • 一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象: 该对象与其依赖的对象紧密耦合。
  • 想定制一个分布在多个类中的行为,而又不想生成太多的子类: 可以将这些行为的协调逻辑移到中介者中,通过改变中介者或创建新的中介者来实现行为变化。
  • GUI 开发中的对话框协调: 如前文例子,对话框(Mediator)负责协调其内部各个控件(Colleagues)之间的交互(如启用/禁用按钮、根据选项显示/隐藏其他控件等)。许多 GUI 框架都隐含或显式地使用了中介者模式。
  • 多方协作系统: 例如,一个聊天室(Mediator)负责接收某个用户(Colleague)发送的消息,并将消息转发给聊天室内的其他所有用户(Colleagues)。用户之间不直接通信。
  • 机场交通管制系统: 塔台(Mediator)负责协调飞机(Colleagues)的起飞、降落、滑行,避免冲突。飞机之间不直接通信。

四、集中协调的实现:中介者模式的 Java 实践

我们用简化的 GUI 对话框例子来实现中介者模式。

1. 定义中介者接口 (Mediator):

java 复制代码
/**
 * 中介者接口:定义了组件之间交互的方法
 */
interface DialogMediator {
    // 当某个组件发生变化时,通知中介者
    void componentChanged(Component component);

    // (可选) 注册组件的方法,如果中介者需要显式管理组件引用
    // void registerComponent(Component component);
}

2. 定义同事抽象类 (Colleague):

(使用抽象类方便持有 Mediator 引用)

java 复制代码
/**
 * 同事抽象类:所有 GUI 组件的基类
 */
abstract class Component {
    protected DialogMediator mediator; // 持有中介者引用
    protected String name;

    public Component(DialogMediator mediator, String name) {
        this.mediator = mediator;
        this.name = name;
        System.out.println("创建组件: " + name + ", 关联中介者: " + (mediator != null ? mediator.getClass().getSimpleName() : "无"));
    }

    public String getName() { return name; }

    // 当自身状态改变时,通知中介者
    public void changed() {
        System.out.println("组件 '" + name + "' 状态改变,通知中介者...");
        if (mediator != null) {
            mediator.componentChanged(this);
        }
    }

    // 提供方法供中介者调用来改变组件状态
    public abstract void update(); // 例如,更新启用/禁用状态
}

3. 创建具体同事类 (ConcreteColleague):

java 复制代码
/**
 * 具体同事A:文本框
 */
class TextBox extends Component {
    private String text = "";
    public TextBox(DialogMediator mediator, String name) { super(mediator, name); }

    public String getText() { return text; }
    public void setText(String text) {
        if (!this.text.equals(text)) {
            this.text = text;
            changed(); // 内容改变,通知中介者
        }
    }

    @Override public void update() { /* 文本框通常不需要根据其他组件更新状态 */ }
    public boolean isEmpty() { return text == null || text.trim().isEmpty(); }
}

/**
 * 具体同事B:复选框
 */
class CheckBox extends Component {
    private boolean checked = false;
    public CheckBox(DialogMediator mediator, String name) { super(mediator, name); }

    public boolean isChecked() { return checked; }
    public void setChecked(boolean checked) {
        if (this.checked != checked) {
            this.checked = checked;
            changed(); // 状态改变,通知中介者
        }
    }

    @Override public void update() { /* 复选框通常不需要根据其他组件更新状态 */ }
}

/**
 * 具体同事C:按钮
 */
class Button extends Component {
    private boolean enabled = false; // 初始禁用
    public Button(DialogMediator mediator, String name) { super(mediator, name); }

    public boolean isEnabled() { return enabled; }
    public void setEnabled(boolean enabled) {
        if (this.enabled != enabled) {
            this.enabled = enabled;
            System.out.println("按钮 '" + name + "' 状态更新为: " + (enabled ? "可用" : "禁用"));
            // 状态改变通常由中介者调用,无需再通知中介者
        }
    }

    // 当按钮被点击时执行的操作 (这里简化,只打印)
    public void click() {
        if (isEnabled()) {
            System.out.println("按钮 '" + name + "' 被点击!执行注册操作...");
            // ... 实际操作 ...
        } else {
            System.out.println("按钮 '" + name + "' 当前被禁用,点击无效。");
        }
    }

    // update 方法由中介者调用,用于更新按钮的可用状态
    @Override
    public void update() {
        // 在这个例子里,update 就是 setEnabled,但名字可以更通用
        // 中介者会根据其他组件状态决定调用 setEnabled(true) 还是 setEnabled(false)
        System.out.println("按钮 '" + name + "' 收到中介者的更新指令 (将在中介者逻辑中设置可用性)");
    }
}

4. 创建具体中介者类 (ConcreteMediator):

java 复制代码
/**
 * 具体中介者:注册对话框
 * 封装了文本框、复选框和按钮之间的交互逻辑
 */
class RegistrationDialog implements DialogMediator {
    // 中介者需要知道它所协调的所有同事对象
    private TextBox usernameTextBox;
    private CheckBox agreeCheckBox;
    private Button registerButton;

    // 提供 Setter 方法让客户端可以设置同事对象 (或者通过构造函数)
    public void setUsernameTextBox(TextBox usernameTextBox) { this.usernameTextBox = usernameTextBox; }
    public void setAgreeCheckBox(CheckBox agreeCheckBox) { this.agreeCheckBox = agreeCheckBox; }
    public void setRegisterButton(Button registerButton) { this.registerButton = registerButton; }

    // 核心方法:当任何一个组件改变时,中介者根据规则协调其他组件
    @Override
    public void componentChanged(Component component) {
        System.out.println("中介者: 收到组件 '" + component.getName() + "' 的变更通知,开始协调...");
        // 根据改变的组件和当前所有组件的状态,来更新注册按钮的可用性
        updateRegisterButtonState();
    }

    // 封装了更新注册按钮状态的逻辑
    private void updateRegisterButtonState() {
        if (usernameTextBox != null && agreeCheckBox != null && registerButton != null) {
            // 规则:用户名不为空 且 同意协议复选框被选中 时,按钮才可用
            boolean canRegister = !usernameTextBox.isEmpty() && agreeCheckBox.isChecked();
            System.out.println("中介者: 计算按钮状态: 用户名非空=" + !usernameTextBox.isEmpty() + ", 同意协议=" + agreeCheckBox.isChecked() + " => 可注册=" + canRegister);
            registerButton.setEnabled(canRegister); // 直接调用按钮的方法更新其状态
        }
    }

    // (可选) 可以添加其他协调方法,比如 simulateRegistration()
    public void simulateRegistration() {
        System.out.println("\n--- 模拟点击注册按钮 ---");
        if (registerButton != null) {
            registerButton.click();
        }
    }
}

5. 客户端使用:

java 复制代码
public class MediatorClient {
    public static void main(String[] args) {
        // 1. 创建中介者对象 (对话框)
        RegistrationDialog dialog = new RegistrationDialog();

        // 2. 创建同事对象 (GUI 组件),并将中介者传递给它们
        TextBox username = new TextBox(dialog, "用户名输入框");
        CheckBox agree = new CheckBox(dialog, "同意协议复选框");
        Button register = new Button(dialog, "注册按钮");

        // 3. 将同事对象注册到中介者中 (通过 Setter)
        dialog.setUsernameTextBox(username);
        dialog.setAgreeCheckBox(agree);
        dialog.setRegisterButton(register);

        // 打印初始状态 (按钮应为禁用)
        System.out.println("初始状态: 用户名='" + username.getText() + "', 同意=" + agree.isChecked() + ", 按钮可用=" + register.isEnabled());

        System.out.println("\n--- 模拟用户操作 ---");

        // 用户输入用户名
        username.setText("Ada"); // TextBox.changed() -> dialog.componentChanged() -> dialog.updateRegisterButtonState() -> button.setEnabled(false) (因为复选框未选)
        System.out.println("输入用户名后: 按钮可用=" + register.isEnabled());

        // 用户勾选同意协议
        agree.setChecked(true); // CheckBox.changed() -> dialog.componentChanged() -> dialog.updateRegisterButtonState() -> button.setEnabled(true)
        System.out.println("勾选同意后: 按钮可用=" + register.isEnabled());

        // 用户模拟点击注册按钮
        dialog.simulateRegistration(); // 调用按钮的 click 方法

        System.out.println("\n--- 模拟用户取消勾选 ---");
        agree.setChecked(false); // CheckBox.changed() -> dialog.componentChanged() -> dialog.updateRegisterButtonState() -> button.setEnabled(false)
        System.out.println("取消勾选后: 按钮可用=" + register.isEnabled());

        // 用户再次模拟点击注册按钮
        dialog.simulateRegistration(); // 按钮被禁用,点击无效

        // 关键点:
        // - TextBox, CheckBox, Button 只与 DialogMediator 交互,彼此不知道对方的存在。
        // - 所有的交互逻辑(如何根据输入和勾选状态更新按钮)都集中在 RegistrationDialog 中。
        // - 添加新的组件(如密码框)或修改交互规则,主要修改 RegistrationDialog 即可,对现有组件影响很小。
    }
}

代码解读:

  • DialogMediator 定义了中介者的接口。Component 是同事的抽象基类,持有中介者引用,并在状态改变时调用 mediator.componentChanged()
  • TextBox, CheckBox, Button 是具体同事,它们只负责自身的状态和行为,并在需要时通知中介者。Button 还提供 setEnabled 方法供中介者调用。
  • RegistrationDialog 是具体中介者,它持有所有同事的引用,并在 componentChanged 方法中实现了核心的协调逻辑(根据文本框和复选框状态更新按钮状态)。
  • 客户端负责创建中介者和同事,并将它们关联起来。之后的所有交互都通过中介者进行。

五、模式的价值:中介者带来的解耦与集中控制

中介者模式的核心价值在于其对复杂交互的管理耦合度的降低

  1. 降低了类间的耦合 (Reduces Coupling): 将原来多对多 的网状依赖关系转变成了一对多的星型依赖关系(每个同事只依赖中介者)。这使得修改一个同事类通常不会影响到其他同事类。
  2. 将交互集中控制 (Centralizes Control): 对象之间的交互逻辑被封装在中介者对象中,使得交互行为更容易理解、维护和修改。
  3. 简化了对象协议 (Simplifies Object Protocols): 同事对象只需要与中介者通信,其接口可以设计得更简单。
  4. 提高了对象的可复用性 (Increases Reusability): 由于同事对象不再依赖于其他特定同事,它们更容易被独立地复用到其他场景或与其他中介者配合使用。
  5. 符合开闭原则(对同事而言): 增加新的同事类通常不需要修改现有的同事类,只需要修改中介者类来协调新的同事即可。(但对中介者本身可能不符合 OCP,见下文)。

六、权衡与考量:中介者模式的"中心化"风险

引入中介者模式也需要注意其潜在的问题:

  1. 中介者可能变得庞大而复杂 (Mediator Complexity): 最主要的缺点是,如果系统中同事对象很多,交互逻辑非常复杂,那么中介者对象自身可能会变成一个庞大、难以维护的"上帝类",承担了过多的职责。这反而可能成为新的瓶颈。需要仔细设计中介者的职责范围,或者考虑将过于复杂的中介者拆分成多个更小的中介者。
  2. 中介者对具体同事的依赖(通常难以避免): 虽然同事对其他同事解耦了,但具体中介者通常需要知道 并引用所有它协调的具体同事类,以便能够调用它们的特定方法。这使得中介者与具体同事之间存在一定的耦合。(可以通过让同事实现更细粒度的接口,让中介者依赖这些接口而非具体类来缓解,但这会增加接口数量)。

七、心法归纳:中心协调,星型通信

中介者模式的核心"心法"在于**"中心化"与"解耦"**:

  1. 中心化交互 (Centralize Interaction): 引入一个中介者 (Mediator) 对象,作为所有相关对象(同事 Colleague)之间通信的唯一枢纽 ,将原本分散的、网状的交互逻辑集中到中介者内部进行管理。
  2. 解耦同事 (Decouple Colleagues): 让所有的同事对象只依赖于中介者 ,而不再相互依赖。它们之间的通信完全通过中介者转发,从而将网状依赖 简化为星型依赖,显著降低了耦合度。

掌握中介者模式,意味着你拥有了:

  • 应对复杂对象交互、降低系统耦合度的有效策略。
  • 将混乱的网状依赖关系重构为清晰的星型结构的能力。
  • 集中管理和维护对象间协作逻辑的方法。
  • 提高组件可复用性的途径。

当你发现系统中对象之间存在着"剪不断,理还乱"的复杂依赖和交互时,中介者模式就是你设计工具箱中那座能够"指挥交通、理顺关系"的"中央控制塔"。但同时也要警惕,避免让这座塔自身变得过于庞大和复杂,适时进行职责拆分是保持其有效性的关键。


下一章预告: 《Java 设计模式心法:解释器 (Interpreter) - 构建领域特定语言的解析引擎》。当我们遇到需要解释执行一种特定"语言"(如数学表达式、查询语句、自定义规则)的场景时,该如何设计一个灵活的解析和执行引擎呢?解释器模式将为我们展示一种基于文法表示来构建解释器的有趣模式。敬请期待!

相关推荐
极客智谷2 分钟前
深入理解Java线程池:从原理到实战的完整指南
java·后端
代码不行的搬运工10 分钟前
HTML快速入门-4:HTML <meta> 标签属性详解
java·前端·html
mask哥1 小时前
详解最新链路追踪skywalking框架介绍、架构、环境本地部署&配置、整合微服务springcloudalibaba 、日志收集、自定义链路追踪、告警等
java·spring cloud·架构·gateway·springboot·skywalking·链路追踪
XU磊2601 小时前
javaWeb开发---前后端开发全景图解(基础梳理 + 技术体系)
java·idea
学也不会1 小时前
雪花算法
java·数据库·oracle
晓华-warm1 小时前
国产免费工作流引擎star 5.9k,Warm-Flow版本升级1.7.0(新增大量好用功能)
java·中间件·流程图·开源软件·flowable·工作流·activities
凭君语未可1 小时前
介绍 IntelliJ IDEA 快捷键操作
java·ide·intellij-idea
麓殇⊙1 小时前
设计模式--桥接模式详解
设计模式·桥接模式
码上飞扬1 小时前
Java大师成长计划之第5天:Java中的集合框架
java·开发语言
24k小善1 小时前
FlinkUpsertKafka深度解析
java·大数据·flink·云计算