Java 设计模式精解:中介者模式及其应用场景

298 阅读17分钟

还记得那次项目"灾难"吗?随着组件增多,代码改一处就得动十处,牵一发动全身。这种 N² 复杂度让维护变的很恶心:10 个组件需管理 45 个交互关系,20 个组件则是 190 个!中介者模式是救星,它通过一个中心化协调者将网状结构转为星状,每个组件只与中介者交互,复杂度降至 O(n),让代码重获清晰和秩序。

什么是中介者模式?

中介者模式是一种行为型设计模式,它通过引入一个中介对象来处理一组对象之间的交互,从而将对象之间的直接引用关系解耦,降低了系统的复杂度。

这种模式与"迪米特法则"(最少知识原则)密切相关,该法则建议一个对象应该尽量少地了解其他对象。中介者模式通过集中管理交互逻辑,让每个组件只需要了解中介者,而不需要了解其他组件的细节,完美践行了这一原则。

想象一下,中介者就像是一个交通指挥中心,各个组件就像是道路上的车辆。如果没有指挥中心,每辆车都需要与其他车辆直接沟通,这会导致混乱。而有了指挥中心后,车辆只需要与指挥中心通信,由指挥中心协调大家的行动。

中介者模式的适用场景

  1. 对象之间存在复杂的网状交互关系:当系统中的对象形成了错综复杂的引用关系时,使用中介者模式可以将这种网状结构转化为星状结构。

  2. 某个对象引用了多个对象并直接与之通信:可以通过引入中介者来减少对象之间的直接依赖。

  3. 多个组件间的协作行为需要集中控制:当组件之间的交互逻辑变得复杂,并且这些交互行为需要集中管理时。

  4. 想要创建可复用的组件,但又不希望这些组件与特定的类直接耦合:通过中介者接口可以降低组件的耦合度,提高复用性。例如,聊天室案例中的User类只依赖于ChatMediator接口而非具体实现,因此可以在不同的聊天系统中复用,只需提供不同的中介者实现即可。

  5. GUI 界面组件的交互管理:如按钮点击、文本框输入、下拉选择等操作需要影响其他界面元素时,可以使用中介者模式集中处理这些交互。

实际案例:聊天室系统

聊天室是中介者模式的经典应用场景。在聊天室中,用户之间不直接通信,而是通过聊天室(中介者)转发消息。

graph TD
    A[聊天室: 中介者] --- B[用户1]
    A --- C[用户2]
    A --- D[用户3]
    A --- E[用户4]

如果没有中介者,用户之间的通信线路会呈指数级增长:

graph TD
    B[用户1] --- C[用户2]
    B --- D[用户3]
    B --- E[用户4]
    C --- D
    C --- E
    D --- E

下面是一个聊天室的实现示例:

// 中介者接口
public interface ChatMediator {
    void sendMessage(String message, User user);
    void addUser(User user);
}

// 具体中介者:聊天室
public class ChatRoom implements ChatMediator {
    // 使用CopyOnWriteArrayList保证多线程环境下的线程安全
    private List<User> users;

    public ChatRoom() {
        // 多线程环境中应该使用:
        // this.users = new CopyOnWriteArrayList<>();
        // 简单示例中使用ArrayList即可
        this.users = new ArrayList<>();
    }

    @Override
    public void addUser(User user) {
        this.users.add(user);
        user.setMediator(this); // 中介者反向设置自身,维持一致性
    }

    @Override
    public void sendMessage(String message, User sender) {
        // 将消息发送给除了发送者之外的所有用户
        for (User user : users) {
            // 使用equals比较而非引用比较,更安全,尤其在分布式或序列化场景
            if (!user.equals(sender)) {
                user.receive(message);
            }
        }
    }
}

// 用户抽象类
public abstract class User {
    protected ChatMediator mediator;
    protected String name;

    public User(String name) {
        this.name = name;
    }

    // 设置中介者,由中介者主动调用
    public void setMediator(ChatMediator mediator) {
        this.mediator = mediator;
    }

    // 显式声明抽象方法,子类必须实现发送消息的逻辑
    public abstract void send(String message);

    // 显式声明抽象方法,子类必须实现接收消息的逻辑
    public abstract void receive(String message);

    // 当状态变更需要通知中介者时调用
    protected void notifyMediator() {
        // 在聊天室例子中不需要主动通知,消息发送时已调用中介者
        // 实际应用中可在此通知中介者状态变化
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

// 普通用户
public class NormalUser extends User {

    public NormalUser(String name) {
        super(name);
    }

    @Override
    public void send(String message) {
        System.out.println(this.name + " 发送消息: " + message);
        mediator.sendMessage(message, this);
    }

    @Override
    public void receive(String message) {
        System.out.println(this.name + " 收到消息: " + message);
    }
}

// 客户端测试代码
public class ChatDemo {
    public static void main(String[] args) {
        ChatMediator chatroom = new ChatRoom();

        User user1 = new NormalUser("张三");
        User user2 = new NormalUser("李四");
        User user3 = new NormalUser("王五");

        // 中介者统一管理组件关系
        chatroom.addUser(user1);
        chatroom.addUser(user2);
        chatroom.addUser(user3);

        user1.send("大家好!");
        user3.send("你好,张三!");
    }
}

运行结果:

张三 发送消息: 大家好!
李四 收到消息: 大家好!
王五 收到消息: 大家好!
王五 发送消息: 你好,张三!
张三 收到消息: 你好,张三!
李四 收到消息: 你好,张三!

另一个实例:航空管制系统

航空管制系统是中介者模式的另一个典型应用。在这个系统中,塔台作为中介者,协调各个飞机的起飞和降落。

sequenceDiagram
    participant 飞机1
    participant 塔台
    participant 飞机2

    飞机1->>塔台: 请求起飞
    塔台->>飞机1: 允许起飞
    飞机2->>塔台: 请求降落
    塔台->>飞机2: 允许降落

代码实现:

// 定义操作类型枚举,提高代码语义清晰度
public enum OperationType {
    LANDING("着陆"),
    TAKEOFF("起飞");

    private String description;

    OperationType(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}

// 中介者接口
public interface AirTrafficControl {
    void addAircraft(Aircraft aircraft);
    void requestLanding(Aircraft aircraft);
    void requestTakeoff(Aircraft aircraft);
}

// 具体中介者:塔台
public class ControlTower implements AirTrafficControl {
    private List<Aircraft> aircrafts;
    private Queue<Aircraft> landingQueue;
    private Queue<Aircraft> takeoffQueue;

    public ControlTower() {
        this.aircrafts = new ArrayList<>();
        // 使用队列而非其他数据结构,确保请求按FIFO顺序处理
        // 这对航空管制至关重要,保证"先请求先服务"的公平性
        this.landingQueue = new LinkedList<>();
        this.takeoffQueue = new LinkedList<>();
    }

    @Override
    public void addAircraft(Aircraft aircraft) {
        aircrafts.add(aircraft);
        aircraft.setAtc(this); // 中介者反向设置自身引用
        System.out.println("塔台: 飞机 " + aircraft.getCallSign() + " 已注册");
    }

    @Override
    public void requestLanding(Aircraft aircraft) {
        landingQueue.offer(aircraft);
        System.out.println("塔台: 收到 " + aircraft.getCallSign() + " 的着陆请求");
        processQueue(landingQueue, OperationType.LANDING);
    }

    @Override
    public void requestTakeoff(Aircraft aircraft) {
        takeoffQueue.offer(aircraft);
        System.out.println("塔台: 收到 " + aircraft.getCallSign() + " 的起飞请求");
        processQueue(takeoffQueue, OperationType.TAKEOFF);
    }

    // 提取通用的队列处理方法,使用枚举替代布尔值,提高可读性
    private void processQueue(Queue<Aircraft> queue, OperationType operation) {
        if (!queue.isEmpty()) {
            Aircraft aircraft = queue.poll();
            System.out.println("塔台: 允许 " + aircraft.getCallSign() + " " + operation.getDescription());

            if (operation == OperationType.LANDING) {
                aircraft.land();
            } else {
                aircraft.takeoff();
            }
        }
    }
}

// 飞机抽象类
public abstract class Aircraft {
    protected AirTrafficControl atc;
    protected String callSign;

    // 构造时不直接设置ATC,由中介者管理注册
    public Aircraft(String callSign) {
        this.callSign = callSign;
    }

    // 仅负责设置内部引用,不主动注册
    public void setAtc(AirTrafficControl atc) {
        this.atc = atc;
    }

    public String getCallSign() {
        return callSign;
    }

    public void requestLanding() {
        System.out.println(callSign + ": 请求着陆");
        atc.requestLanding(this);
    }

    public void requestTakeoff() {
        System.out.println(callSign + ": 请求起飞");
        atc.requestTakeoff(this);
    }

    public abstract void land();
    public abstract void takeoff();
}

// 客机
public class Airliner extends Aircraft {

    public Airliner(String callSign) {
        super(callSign);
    }

    @Override
    public void land() {
        System.out.println(callSign + ": 客机正在着陆");
    }

    @Override
    public void takeoff() {
        System.out.println(callSign + ": 客机正在起飞");
    }
}

// 测试代码
public class ATCDemo {
    public static void main(String[] args) {
        AirTrafficControl tower = new ControlTower();

        Aircraft flight1 = new Airliner("CA1234");
        Aircraft flight2 = new Airliner("MU5678");

        // 中介者负责管理组件,而非组件自行注册
        tower.addAircraft(flight1);
        tower.addAircraft(flight2);

        flight1.requestTakeoff();
        flight2.requestLanding();
    }
}

运行结果:

塔台: 飞机 CA1234 已注册
塔台: 飞机 MU5678 已注册
CA1234: 请求起飞
塔台: 收到 CA1234 的起飞请求
塔台: 允许 CA1234 起飞
CA1234: 客机正在起飞
MU5678: 请求着陆
塔台: 收到 MU5678 的着陆请求
塔台: 允许 MU5678 着陆
MU5678: 客机正在着陆

GUI 组件交互的中介者模式

在 GUI 应用中,中介者模式尤为常见。想象一个登录表单,用户名、密码输入框和登录按钮需要协同工作:

// 中介者接口
public interface FormMediator {
    void componentChanged(UIComponent component);
    void registerComponent(UIComponent component);
}

// UI组件抽象类
public abstract class UIComponent {
    protected FormMediator mediator;

    public void setMediator(FormMediator mediator) {
        this.mediator = mediator;
    }

    // 当组件状态变化时通知中介者,更清晰的命名
    protected void notifyMediator() {
        if (mediator != null) {
            mediator.componentChanged(this);
        }
    }
}

// 具体组件:用户名输入框
public class UsernameField extends UIComponent {
    private String username = "";

    public void setUsername(String username) {
        this.username = username;
        notifyMediator(); // 通知中介者状态已变更
    }

    public String getUsername() {
        return username;
    }
}

// 具体组件:密码输入框
public class PasswordField extends UIComponent {
    private String password = "";

    public void setPassword(String password) {
        this.password = password;
        notifyMediator(); // 通知中介者状态已变更
    }

    public String getPassword() {
        return password;
    }
}

// 具体组件:登录按钮
public class LoginButton extends UIComponent {
    private boolean enabled = false;

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

    public boolean isEnabled() {
        return enabled;
    }

    public void click() {
        if (enabled) {
            System.out.println("执行登录操作...");
        }
    }
}

// 具体中介者:登录表单
public class LoginFormMediator implements FormMediator {
    private UsernameField usernameField;
    private PasswordField passwordField;
    private LoginButton loginButton;

    // 通用组件注册方法,简化注册流程
    @Override
    public void registerComponent(UIComponent component) {
        if (component instanceof UsernameField) {
            this.usernameField = (UsernameField) component;
        } else if (component instanceof PasswordField) {
            this.passwordField = (PasswordField) component;
        } else if (component instanceof LoginButton) {
            this.loginButton = (LoginButton) component;
        }
        component.setMediator(this);
    }

    @Override
    public void componentChanged(UIComponent component) {
        // 根据用户名和密码是否都已输入来启用或禁用登录按钮
        if (component instanceof UsernameField || component instanceof PasswordField) {
            boolean bothFilled = !usernameField.getUsername().isEmpty()
                              && !passwordField.getPassword().isEmpty();
            loginButton.setEnabled(bothFilled);
            System.out.println("登录按钮状态: " + (bothFilled ? "启用" : "禁用"));
        }
    }
}

// 测试代码
public class LoginFormDemo {
    public static void main(String[] args) {
        // 创建组件
        UsernameField usernameField = new UsernameField();
        PasswordField passwordField = new PasswordField();
        LoginButton loginButton = new LoginButton();

        // 创建中介者并关联组件
        FormMediator mediator = new LoginFormMediator();
        mediator.registerComponent(usernameField);
        mediator.registerComponent(passwordField);
        mediator.registerComponent(loginButton);

        // 模拟用户输入
        System.out.println("用户输入用户名...");
        usernameField.setUsername("admin");

        System.out.println("用户输入密码...");
        passwordField.setPassword("123456");

        // 尝试点击登录
        loginButton.click();
    }
}

运行结果:

用户输入用户名...
登录按钮状态: 禁用
用户输入密码...
登录按钮状态: 启用
执行登录操作...

在这个例子中,中介者模式使得各个 UI 组件不需要直接引用彼此。只有中介者知道"当用户名和密码都填写后才能启用登录按钮"这一业务规则,组件本身只负责通知中介者自己的状态变化。这样的设计使 UI 组件可以轻松复用在其他表单中。

中介者模式在 Spring 框架中的应用

Spring 框架中的事件机制就是中介者模式的一种应用。ApplicationContext(实现了 ApplicationEventPublisher 接口)作为中介者,协调各个 bean 之间的事件发布与监听,避免了事件发布者与监听者之间的直接耦合。

// 事件类
public class UserCreatedEvent extends ApplicationEvent {
    private static final long serialVersionUID = 1L;
    private String username;

    public UserCreatedEvent(Object source, String username) {
        super(source);
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}

// 事件发布者
@Service
public class UserService {
    @Autowired
    private ApplicationEventPublisher publisher; // ApplicationContext实现了此接口

    public void createUser(String username) {
        System.out.println("创建用户: " + username);
        // 发布者无需关心谁在监听,只负责发布事件
        publisher.publishEvent(new UserCreatedEvent(this, username));
    }
}

// 事件监听者
@Component
public class EmailService {
    @EventListener
    public void handleUserCreated(UserCreatedEvent event) {
        System.out.println("发送欢迎邮件给: " + event.getUsername());
    }
}

// 异步事件监听者
@Component
public class LogService {
    @EventListener
    @Async  // 标记为异步处理,不阻塞主线程
    public void handleUserCreated(UserCreatedEvent event) {
        System.out.println("记录用户创建日志: " + event.getUsername());
    }
}

在这个例子中,UserService 不需要直接引用 EmailService 和 LogService,而是通过 ApplicationContext 这个中介者来协调它们的交互。Spring 的事件机制背后实际上是一个典型的发布-订阅模型,其中:

  1. ApplicationEventPublisher接口作为中介者 API,允许组件发布事件
  2. ApplicationContext实现该接口,内部维护了事件与监听者的映射
  3. 监听器注册机制:
  • 实现ApplicationListener接口的 Bean 会自动注册为监听器
  • @EventListener注解标记的方法会被 Spring 通过适配器转换为监听器
  • Spring 内部负责事件的多播分发,实际承担中介者的核心功能

值得注意的是,Spring 还支持异步事件处理(通过@Async注解),让中介者可以协调异步交互,进一步降低系统耦合。

中介者模式的不同形式

简化中介者(无接口)

在小型应用或组件间关系相对稳定的情况下,可以省略中介者接口,直接使用具体中介者:

// 简化中介者(无接口)
public class SimpleDialogMediator {
    private Button okButton;
    private Button cancelButton;
    private Checkbox checkbox;

    // 直接引用具体组件,无需接口抽象
    public void registerComponent(Object component) {
        if (component instanceof Button) {
            if ("OK".equals(((Button)component).getText())) {
                this.okButton = (Button)component;
            } else {
                this.cancelButton = (Button)component;
            }
        } else if (component instanceof Checkbox) {
            this.checkbox = (Checkbox)component;
        }
    }

    // 处理组件通知
    public void componentClicked(Object component) {
        if (component == okButton) {
            // 处理确定按钮点击
        } else if (component == cancelButton) {
            // 处理取消按钮点击
        } else if (component == checkbox) {
            // 处理复选框状态变化
        }
    }
}

这种简化方式牺牲了一定的扩展性和灵活性,但在简单场景下可以减少代码量。

分布式协调器

在微服务架构中,API 网关可以视为一种分布式协调器,协调不同服务间的交互:

// API网关作为分布式协调器
@RestController
public class ApiGateway {
    @Autowired
    private UserService userService;

    @Autowired
    private OrderService orderService;

    @Autowired
    private PaymentService paymentService;

    @PostMapping("/api/orders")
    public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
        // 验证用户
        UserDto user = userService.validateUser(request.getUserId());

        // 创建订单
        OrderDto order = orderService.createOrder(user, request.getItems());

        // 处理支付
        PaymentDto payment = paymentService.processPayment(order, request.getPaymentInfo());

        // 返回组合结果
        return ResponseEntity.ok(new OrderResponse(order, payment));
    }
}

在分布式系统中,API 网关需要定义清晰的接口契约(如 REST API 规范),避免成为服务间交互的紧耦合点。良好的接口设计应该隐藏内部实现细节,只暴露必要的操作和参数。

中介者模式 vs 观察者模式

中介者模式和观察者模式都用于解耦对象之间的交互,但它们有着本质的区别:

特性中介者模式观察者模式
交互方式中心化控制(组件通过中介者交互)广播式通知(主题向所有观察者发送通知)
通信方向多向(任何组件都可以通过中介者与其他组件交互)单向(主题向观察者通知)
组件关系组件不知道其他组件的存在观察者知道主题,但主题不一定知道具体观察者
控制流组件 → 中介者 → 组件(双向控制)主题 → 观察者(单向通知)
职责边界负责协调多个组件的复杂交互逻辑(如过滤、顺序控制)仅负责单向通知,不处理观察者之间的协作
控制复杂度中介者可能变得复杂主题相对简单,复杂性分散在各观察者中
适用场景组件间有复杂的交互逻辑一对多的依赖关系,当一个对象状态改变时通知其他对象

场景对比示例

聊天室(适合中介者模式)

// 中介者模式:用户之间通过聊天室交互
public class ChatRoom {
    public void sendMessage(String message, User sender) {
        // 中介者知道如何将消息从一个用户传递给其他用户
        for (User user : userList) {
            if (!user.equals(sender)) {
                user.receive(message);
            }
        }
    }
}

股票行情推送(适合观察者模式)

// 观察者模式:股票价格变化时通知所有订阅者
public class StockMarket {
    private List<StockObserver> observers = new ArrayList<>();

    public void addObserver(StockObserver observer) {
        observers.add(observer);
    }

    public void updatePrice(String stock, double price) {
        // 股票价格变动时,主题向所有观察者广播通知
        for (StockObserver observer : observers) {
            observer.update(stock, price);
        }
    }
}

核心区别:中介者知道组件之间如何交互并控制交互流程,而观察者模式中的主题只负责单向通知,不关心观察者之间的关系。

中介者模式的优缺点

优点

  1. 降低了系统的耦合度:各组件之间不再直接交互,而是通过中介者进行。
  2. 简化了对象之间的交互:将多对多的交互转变为一对多的交互。
  3. 提高了系统的可维护性:各组件的修改不会影响其他组件。
  4. 符合开闭原则:增加新组件只需要修改中介者,而不影响现有组件。

缺点

  1. 中介者可能变得过于庞大:随着系统规模的扩大,中介者可能会过于复杂,难以维护,尤其是当交互逻辑包含复杂业务规则或跨领域协作时。

判断中介者是否过于臃肿的标准:

  • 代码行数超过 300 行
  • 包含超过 10 个以上的条件分支(if-else/switch)
  • 处理 3 个以上不同领域的业务逻辑
  • 方法参数过多(超过 4 个)
  1. 把交互复杂性转移到了中介者:虽然降低了组件之间的耦合,但增加了中介者的复杂度。

如何避免中介者臃肿

下面是一个示例,展示如何拆分臃肿的聊天室中介者:

// 将ChatRoom拆分为两个职责明确的中介者

// 1. 消息中介者 - 负责消息传递
public interface MessageMediator {
    void sendMessage(String message, User sender);
}

// 2. 用户管理中介者 - 负责用户管理
public interface UserMediator {
    void addUser(User user);
    void removeUser(User user);
}

// 组合实现
public class ChatRoomManager implements MessageMediator, UserMediator {
    private MessageHandler messageHandler = new MessageHandler();
    private UserManager userManager = new UserManager();

    @Override
    public void sendMessage(String message, User sender) {
        // 委托给专门的消息处理器
        messageHandler.sendMessage(message, sender, userManager.getUsers());
    }

    @Override
    public void addUser(User user) {
        // 委托给专门的用户管理器
        userManager.addUser(user);
        user.setMediator(this); // 保持双向关联
    }

    @Override
    public void removeUser(User user) {
        userManager.removeUser(user);
    }

    // 可以通过构造函数注入不同的处理器
    public void setMessageHandler(MessageHandler messageHandler) {
        this.messageHandler = messageHandler;
    }
}

// 消息处理策略
public class MessageHandler {
    public void sendMessage(String message, User sender, List<User> users) {
        for (User user : users) {
            if (!user.equals(sender)) {
                user.receive(message);
            }
        }
    }
}

// 用户管理
public class UserManager {
    private List<User> users = new ArrayList<>();

    public void addUser(User user) {
        users.add(user);
    }

    public void removeUser(User user) {
        users.remove(user);
    }

    public List<User> getUsers() {
        return Collections.unmodifiableList(users);
    }
}

这种拆分方式遵循以下原则:

  1. 单一职责:每个中介者只负责一类交互逻辑
  2. 组合优于继承:使用组合方式集成多个专门的处理器
  3. 策略模式结合:将变化的部分(如消息处理逻辑)抽取为策略类

中介者模式的使用建议

  1. 中介者职责单一:中介者应只负责协调组件交互,不应包含组件自身的业务逻辑。

  2. 通过接口依赖:组件应依赖中介者接口而非具体实现,保持松耦合。例如:

// 错误方式:组件直接依赖具体中介者
public class Button {
    private LoginFormMediator mediator; // 直接依赖具体实现
}

// 正确方式:组件依赖中介者接口
public class Button {
    private FormMediator mediator; // 依赖接口

    public void setMediator(FormMediator mediator) {
        this.mediator = mediator;
    }
}
  1. 避免双向强依赖:组件和中介者之间的关系应该清晰,中介者负责管理组件,而组件只与中介者接口交互,不应了解中介者内部实现细节。

  2. 适度使用:不是所有组件交互都需要中介者,对于简单的一对一交互,直接引用可能更高效。

  3. 测试策略:由于中介者集中了交互逻辑,应通过全面的单元测试覆盖所有组件交互场景,包括边界条件和异常情况。测试中应该:

  • 模拟各种组件状态变化
  • 验证中介者的正确响应
  • 检查交互顺序是否符合预期
  • 确保中介者能处理异常情况
  1. 结合其他设计模式
  • 使用工厂模式创建中介者实例
  • 使用单例模式确保中介者的唯一性
  • 使用命令模式封装交互请求
  • 使用策略模式处理不同的交互策略

总结

项目内容
模式名称中介者模式
类型行为型设计模式
核心思想通过中介者对象集中处理对象之间的交互,降低耦合度
主要角色中介者接口(Mediator)、具体中介者(ConcreteMediator)、同事接口(Colleague)、具体同事类(ConcreteColleague)
同事接口定义同事类与中介者交互的接口,通常包含 setMediator()方法及状态变更通知方法(如 notifyMediator())
适用场景对象之间存在复杂交互关系;需要集中控制组件间协作;GUI 组件交互
优点降低耦合度;简化交互;提高可维护性和可扩展性
缺点中介者可能过于庞大复杂;转移了复杂性而非消除
常见应用聊天室、航空管制系统、GUI 框架、MVC 架构的控制器、消息中间件、API 网关