每天认识一个设计模式-中介者模式:复杂交互的交通枢纽

一、前言:网状耦合的困境

在分布式系统和微服务架构日益普及的当下,系统内各组件间的通信与协作变得愈发复杂。以一个电商系统为例,当用户下单时,订单服务不仅要与库存服务确认商品库存,还要与支付服务对接完成支付流程,同时通知物流服务准备发货 ,各个服务之间直接的通信调用形成了错综复杂的网状结构。​

这种网状依赖就像一张密密麻麻的蜘蛛网,每个服务都是网上的一个节点,彼此相互牵连。一旦其中某个服务进行升级或修改,比如库存服务的接口发生变化,那么订单服务、物流服务等与之直接通信的服务都需要相应地调整代码来适应这种变化。这就如同牵一发而动全身,一个小小的改动可能引发整个系统的不稳定,使得系统的维护和扩展变得异常艰难,严重影响了系统的灵活性和可维护性,这便是我们面临的核心矛盾 —— 对象间网状依赖导致系统僵化。​

为了解决这一困境,建立中心化协调机制显得尤为重要。就像交响乐团需要指挥家来统一协调各个乐器组的演奏,才能呈现出和谐美妙的音乐一样,在软件系统中,我们也需要一个类似 “指挥家” 的角色来管理和协调各组件间的交互,这便是中介者模式的核心价值所在。

在现实生活中,有许多场景都体现了这种中心化协调机制的优势。比如航空管制系统,每架飞机都与塔台保持通信,塔台作为中介者,根据飞机的位置、飞行计划和空域情况等信息,统一调度飞机的起降和飞行路线,确保整个机场的空中交通有序进行。中介者模式便是可以通过引入一个中心协调者,将复杂的网状结构转化为简单的星型结构,从而降低系统复杂度,提升整体的运行效率和稳定性的模式。咱们一起来看看吧~

二、中介者模式的定义与结构解析

中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性,属于行为型模式。中介者模式定义了一个中介对象来封装一系列对象之间的交互,使得这些对象之间不再需要直接相互引用,从而降低了它们之间的耦合度,并且可以独立地改变对象之间的交互方式 。下面我们可以通过其结构图一起来了解一下:

每天认识一个设计模式-中介者模式:复杂交互的交通枢纽_第1张图片

这里我们可以看看到,其实主要包含以下四个核心角色:

Mediator(抽象中介者):定义协调方法接口,规范同事类如何通过中介者通信。它就像是一个抽象的 “协调规则制定者”,只规定了同事类与中介者交互的方式,不涉及具体的协调逻辑 。

Colleague(抽象同事类):持有中介者引用,提供通用的发送 / 接收消息接口。它是所有具体同事类的抽象基类,每个具体同事类都通过它与中介者进行交互 。

ConcreteMediator(具体中介者):维护同事列表,实现具体的协调逻辑。它知道所有具体同事类,并负责协调它们之间的交互 。

ConcreteColleague(具体同事类):封装业务行为,通过中介者与其他同事协作。每个具体同事类只关注自身的业务逻辑,而与其他同事类的交互则通过中介者来完成 。

所以我们如果构建这个模式可根据每个角色的定位来定义,具体实现方式的流程如下:

  1. 定义中介者接口:规定中介者必须实现的接口,定义了同事对象与中介者交互的方法,为具体中介者提供统一的契约,确保不同的具体中介者实现具有一致性,便于同事类与中介者进行交互。​
  2. 创建具体中介者:实现中介者接口,包含协调各同事对象交互的逻辑。它负责管理同事对象的引用,接收同事对象的请求,并根据具体的业务逻辑来协调同事对象之间的交互,从而避免同事对象之间的直接依赖和复杂的网状结构。​
  3. 定义同事类:各个同事类不需要显式地相互引用,而是通过中介者来进行交互。同事类持有中介者的引用,当需要与其他同事类交互时,通过调用中介者的方法来间接实现,将交互逻辑委托给中介者处理,降低了同事类之间的耦合度,提高了系统的可维护性和可扩展性。

三、中介者模式的适用场景与框架实践 

3.1.典型应用场景​

GUI 组件交互:在一个图形化用户界面中,有一个注册表单,包含用户名输入框、密码输入框、确认密码输入框和注册按钮 。当用户在用户名输入框中输入内容时,注册按钮的状态可能需要根据用户名是否符合格式要求来决定是否可用;当用户输入密码和确认密码后,需要验证两者是否一致,并相应地提示用户。

如果这些组件之间直接进行交互,代码会变得非常复杂且难以维护 。通过中介者模式,我们可以创建一个FormMediator中介者类。

public class FormMediator {
    private JTextField usernameField;
    private JPasswordField passwordField;
    private JPasswordField confirmPasswordField;
    private JButton registerButton;

    public FormMediator(JTextField usernameField, JPasswordField passwordField, JPasswordField confirmPasswordField, JButton registerButton) {
        this.usernameField = usernameField;
        this.passwordField = passwordField;
        this.confirmPasswordField = confirmPasswordField;
        this.registerButton = registerButton;
    }

    public void checkUsername() {
        String username = usernameField.getText();
        boolean isValid = username.matches("^[a-zA-Z0-9]{3,20}$");
        registerButton.setEnabled(isValid);
    }

    public void checkPasswords() {
        String password = new String(passwordField.getPassword());
        String confirmPassword = new String(confirmPasswordField.getPassword());
        boolean isValid = password.equals(confirmPassword);
        if (isValid) {
            // 可以添加更多密码强度验证逻辑
            registerButton.setEnabled(true);
        } else {
            registerButton.setEnabled(false);
        }
    }
}

 然后在各个组件的事件处理逻辑中,通过中介者来协调:

usernameField.getDocument().addDocumentListener(new DocumentListener() {
    @Override
    public void insertUpdate(DocumentEvent e) {
        mediator.checkUsername();
    }
    // 省略其他方法
});

passwordField.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        mediator.checkPasswords();
    }
});

分布式系统协调:在一个分布式电商系统中,订单服务、库存服务、支付服务等多个微服务之间需要进行通信和协作。

以订单创建流程为例,当用户下单时,订单服务需要调用库存服务检查库存,调用支付服务进行支付处理 。如果这些服务之间直接进行通信,会形成复杂的网状结构,难以维护和扩展 。引入服务注册中心(如 Eureka、Consul)作为中介者,服务之间通过注册中心进行服务发现和通信。

比如订单服务在注册中心获取库存服务和支付服务的地址,然后进行远程调用 。下面是使用 Feign 客户端通过 Eureka 注册中心调用库存服务的简单示例:

 

@FeignClient(name = "inventory-service")
public interface InventoryClient {
    @GetMapping("/inventory/check/{productId}/{quantity}")
    boolean checkStock(@PathVariable("productId") Long productId, @PathVariable("quantity") Integer quantity);
}

@Service
public class OrderService {
    private final InventoryClient inventoryClient;

    public OrderService(InventoryClient inventoryClient) {
        this.inventoryClient = inventoryClient;
    }

    public boolean createOrder(Order order) {
        // 检查库存
        boolean hasStock = inventoryClient.checkStock(order.getProductId(), order.getQuantity());
        if (hasStock) {
            // 处理订单创建逻辑
            return true;
        }
        return false;
    }
}

工作流引擎:在一个审批工作流中,有提交申请、部门审核、财务审核、总经理审批等多个任务节点 。每个任务节点的状态变化(如提交、通过、拒绝)都可能影响其他节点的执行 。使用中介者模式,工作流引擎可以作为中介者,协调各个任务节点之间的交互 。

例如,当部门审核通过后,工作流引擎通知财务审核节点开始执行 。以下是一个简单的工作流引擎中介者实现:

public class WorkflowEngine {
    private Map taskNodes = new HashMap<>();

    public void registerTaskNode(String nodeName, TaskNode taskNode) {
        taskNodes.put(nodeName, taskNode);
        taskNode.setWorkflowEngine(this);
    }

    public void notify(String fromNode, String event) {
        // 根据事件和来源节点决定通知哪些节点
        if ("departmentApproved".equals(event)) {
            TaskNode financeNode = taskNodes.get("financeApproval");
            if (financeNode != null) {
                financeNode.execute();
            }
        }
    }
}

public abstract class TaskNode {
    private WorkflowEngine workflowEngine;

    public void setWorkflowEngine(WorkflowEngine workflowEngine) {
        this.workflowEngine = workflowEngine;
    }

    public abstract void execute();
}

public class DepartmentApprovalNode extends TaskNode {
    @Override
    public void execute() {
        // 部门审核逻辑
        boolean approved = true; // 假设审核通过
        if (approved) {
            workflowEngine.notify("departmentApproval", "departmentApproved");
        }
    }
}

微服务通信:在一个微服务架构的电商系统中,订单服务、商品服务、用户服务等多个微服务之间需要进行通信 。以用户下单后,订单服务通知商品服务减少库存为例,如果直接调用,会导致服务间耦合度高 。

采用事件总线模式(如 Kafka、RabbitMQ)作为中介者,订单服务在用户下单成功后,发送一个 “订单创建” 事件到事件总线 ,商品服务监听该事件,当接收到事件后,执行减少库存的操作 。下面是使用 Spring Cloud Stream 和 Kafka 实现事件驱动的简单示例:

// 订单服务发送事件
@Service
public class OrderService {
    private final MessageChannel orderOutput;

    public OrderService(MessageChannel orderOutput) {
        this.orderOutput = orderOutput;
    }

    public void createOrder(Order order) {
        // 处理订单创建逻辑
        orderOutput.send(MessageBuilder.withPayload(order).build());
    }
}

// 商品服务接收事件
@Service
public class ProductService {
    @StreamListener("orderInput")
    public void handleOrder(Order order) {
        // 减少库存逻辑
    }
}

3.2.开源框架应用

Spring 框架:Spring 框架中的ApplicationContext就像是一个强大的中介者,它管理着所有的 Bean,协调它们的生命周期和依赖关系 。当一个 Bean 需要依赖另一个 Bean 时,不需要直接创建和引用,而是通过ApplicationContext来获取 。

在我们常见的Spring 的 Web 应用中,UserService依赖于UserRepository: 

@Service
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User findUserById(Long id) {
        return userRepository.findById(id);
    }
}

Spring 容器(ApplicationContext)负责创建UserRepository和UserService的实例,并将UserRepository注入到UserService中,使得UserService无需关心UserRepository的创建和管理细节 。

Apache Camel:Apache Camel 的路由中介功能十分强大,它可以实现不同协议的消息转换和路由 。比如在一个企业集成项目中,需要将来自 HTTP 接口的 JSON 格式订单数据转换为 JMS 队列可接受的 XML 格式并发送 。通过 Apache Camel 的路由配置,可以轻松实现:


    
        
        
        
        
    

这里,Apache Camel 作为中介者,接收 HTTP 请求,进行消息格式转换,然后将转换后的消息发送到 JMS 队列,实现了不同系统间的通信和协作 。

Netty:Netty 中的ChannelPipeline是处理消息编解码流程的中介者 。当客户端发送消息到服务端时,消息会依次经过ChannelPipeline中的各个ChannelHandler 。

例如,在一个基于 Netty 的 RPC 框架中,需要对消息进行序列化和反序列化处理 。可以自定义ChannelHandler并添加到ChannelPipeline中: 

public class RpcServerInitializer extends ChannelInitializer {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new ObjectEncoder());
        pipeline.addLast(new ObjectDecoder());
        pipeline.addLast(new RpcServerHandler());
    }
}

在模式应用上,ChannelPipeline充当了中介者的角色。它封装了各个ChannelHandler之间的交互,使得这些ChannelHandler不需要直接相互引用,降低了它们之间的耦合度。

具体到代码中,ObjectEncoder和ObjectDecoder不需要知道彼此的存在,它们只需要关注自己的编码和解码逻辑。而ChannelPipeline负责协调它们的执行顺序,确保消息能够按照正确的流程进行处理。例如,ObjectEncoder完成编码后,ChannelPipeline会自动将编码后的消息传递给ObjectDecoder进行解码。

同样,RpcServerHandler也只需要关注业务逻辑的处理,不需要关心消息是如何编码和解码的。ChannelPipeline作为中介者,将解码后的消息传递给RpcServerHandler,并在必要时将RpcServerHandler处理后的结果传递回客户端。

这样ObjectEncoder和ObjectDecoder分别负责消息的编码和解码,ChannelPipeline协调它们的执行顺序,使得消息能够正确地在网络中传输和处理 。这种基于中介者模式的设计,使得代码结构更加清晰,易于维护和扩展。

Activiti:Activiti 工作流引擎的任务分配器是协调流程节点间执行顺序的关键中介者 。在一个请假审批流程中,Activiti 根据流程定义,将请假申请任务分配给相应的审批人 。当审批人完成审批后,任务分配器根据审批结果决定下一个任务节点的执行 。以下是一个简单的 Activiti 流程定义(BPMN 2.0 XML)示例:



    
        
        
        
        
        
    

接下来,使用 Java 代码结合 Activiti API 来展示中介者模式参与的流程:

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngines;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.task.Task;

public class ActivitiMediatorPatternExample {

    public static void main(String[] args) {
        // 得到流程引擎
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        
        // 运行时服务,用于启动流程实例
        RuntimeService runtimeService = processEngine.getRuntimeService();
        
        // 任务服务,用于管理和操作任务
        TaskService taskService = processEngine.getTaskService();
        
        // 启动流程实例,这里的leaveApprovalProcess是BPMN文件中定义的流程ID
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leaveApprovalProcess");
        System.out.println("流程实例ID: " + processInstance.getId());
        
        // 获取第一个任务(提交请假申请)
        Task applyTask = taskService.createTaskQuery()
                                     .processInstanceId(processInstance.getId())
                                     .taskName("提交请假申请")
                                     .singleResult();
        System.out.println("当前任务: " + applyTask.getName() + ",任务ID: " + applyTask.getId());
        
        // 模拟员工完成提交请假申请任务
        taskService.complete(applyTask.getId());
        System.out.println("员工已完成提交请假申请任务");
        
        // 获取第二个任务(经理审批)
        Task managerApprovalTask = taskService.createTaskQuery()
                                              .processInstanceId(processInstance.getId())
                                              .taskName("经理审批")
                                              .singleResult();
        System.out.println("当前任务: " + managerApprovalTask.getName() + ",任务ID: " + managerApprovalTask.getId());
        
        // 模拟经理完成审批任务
        taskService.complete(managerApprovalTask.getId());
        System.out.println("经理已完成审批任务");
    }
}

这里ProcessEngine是 Activiti 的核心对象,负责管理和执行流程定义。​RuntimeService用于启动流程实例,相当于中介者模式中的中介者对象,协调流程的启动。​TaskService用于管理和操作任务,它根据流程定义和执行状态,分配任务给相应的参与者,就像中介者协调对象之间的交互一样。​

通过这种方式,Activiti 的任务分配器(通过TaskService体现)作为中介者,协调了 “提交请假申请” 和 “经理审批” 这两个任务节点的执行顺序,确保整个请假审批流程的有序进行,符合中介者模式的设计理念。

3.3中介者模式的应用建议

推荐场景:当业务场景中交互对象超过 5 个时,使用中介者模式可以显著降低对象间的耦合度,提高系统的可维护性和扩展性 。

当一个大型电商平台的订单处理模块,涉及订单服务、库存服务、支付服务、物流服务、用户服务等多个服务之间的复杂交互,此时我们引入中介者模式可以有效地管理这些服务之间的通信和协作。​

⚠️谨慎使用:而对于简单的 CRUD 操作,使用中介者模式可能会导致过度设计 。

比如一个小型的用户管理系统,只包含基本的用户信息增删改查功能,直接在业务逻辑层和数据访问层进行交互即可,无需引入中介者模式增加系统的复杂性 。​所以非必要不要过度去刻意应用设计模式。

最佳实践:结合 CQRS(Command Query Responsibility Segregation)模式实现事件驱动架构是一种非常有效的实践方式 。CQRS 模式将命令和查询的职责分离,通过中介者模式(如事件总线)来处理命令的分发和事件的传播 。

在一个电商系统中,当用户下单时,发送一个 “订单创建” 命令,通过中介者将命令分发给相关的服务(如库存服务、支付服务)进行处理,同时产生 “订单创建成功” 等事件,其他服务可以监听这些事件并做出相应的反应,实现了系统的高内聚、低耦合和可扩展性 。

四、中介者模式的简单应用:订单状态协同变更 

在电商业务中,订单状态变更时,库存服务需要扣减或增加库存,日志服务要记录订单状态变更的详细信息,通知服务则要向用户发送订单状态变更通知,如支付成功通知、发货通知等。

若这些服务之间直接耦合,比如订单服务直接调用库存服务的扣减库存方法、调用日志服务的记录方法、调用通知服务的发送通知方法,当其中任何一个服务的接口或实现发生变化时,订单服务都需要进行相应修改,这会导致代码的维护成本急剧增加,系统的可扩展性和灵活性也会受到极大限制 。 此时我们就可以通过中介者模式的构建方式结合Spring Events 来实现订单状态协同变更。

首先我们先定义订单状态枚举和订单实体类:

public enum OrderState {
    CREATED, PAID, SHIPPED, COMPLETED, CANCELED
}

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String orderNumber;
    @Enumerated(EnumType.STRING)
    private OrderState state;
    // 其他订单相关属性和方法
}

 接着,创建中介者接口OrderMediator:

public interface OrderMediator {
    void notifyStateChange(Order order, OrderState newState);
}

然后,实现具体中介者OrderStateMediator,它依赖于库存服务InventoryService、日志服务AuditLogService和通知服务NotificationService:

@Component
public class OrderStateMediator implements OrderMediator {
    @Autowired
    private InventoryService inventory;
    @Autowired
    private AuditLogService logger;
    @Autowired
    private NotificationService notifier;

    @Override
    @Transactional
    public void notifyStateChange(Order order, OrderState newState) {
        switch (newState) {
            case PAID:
                inventory.updateStock(order);
                logger.logOrderEvent(order, "PAID");
                notifier.sendPaymentConfirm(order.getUser());
                break;
            case SHIPPED:
                inventory.reduceStock(order);
                logger.logOrderEvent(order, "SHIPPED");
                notifier.sendShippingNotification(order.getUser());
                break;
            case CANCELED:
                inventory.restoreStock(order);
                logger.logOrderEvent(order, "CANCELED");
                notifier.sendCancellationNotification(order.getUser());
                break;
            // 其他状态处理逻辑
            default:
                break;
        }
    }
}

以库存服务InventoryService为例,其实现类如下:

@Service
public class InventoryService {
    public void updateStock(Order order) {
        // 根据订单信息更新库存逻辑,如查询库存、扣减库存等
        System.out.println("库存更新,订单:" + order.getOrderNumber());
    }

    public void reduceStock(Order order) {
        // 扣减库存逻辑
        System.out.println("库存扣减,订单:" + order.getOrderNumber());
    }

    public void restoreStock(Order order) {
        // 恢复库存逻辑
        System.out.println("库存恢复,订单:" + order.getOrderNumber());
    }
}

 订单服务OrderService通过注入OrderMediator来实现订单状态变更的协调:

@Service
public class OrderService {
    private final OrderMediator mediator;
    private final OrderRepository orderRepository;

    @Autowired
    public OrderService(OrderMediator mediator, OrderRepository orderRepository) {
        this.mediator = mediator;
        this.orderRepository = orderRepository;
    }

    @Transactional
    public void updateOrderState(Long orderId, OrderState state) {
        Order order = orderRepository.findById(orderId)
               .orElseThrow(() -> new OrderNotFoundException("订单未找到,ID:" + orderId));
        order.setState(state);
        mediator.notifyStateChange(order, state);
    }
}

测试代码如下:

@SpringBootTest
public class OrderServiceTest {
    @Autowired
    private OrderService orderService;
    @Autowired
    private OrderRepository orderRepository;

    @Test
    public void testUpdateOrderStateToPaid() {
        // 创建订单
        Order order = new Order();
        order.setOrderNumber("20241010001");
        order.setState(OrderState.CREATED);
        orderRepository.save(order);

        // 更新订单状态为已支付
        orderService.updateOrderState(order.getId(), OrderState.PAID);

        // 断言订单状态已更新
        Order updatedOrder = orderRepository.findById(order.getId()).orElse(null);
        assertNotNull(updatedOrder);
        assertEquals(OrderState.PAID, updatedOrder.getState());
    }
}

实现要点​:

依赖注入:利用 Spring IoC 容器的依赖注入功能,将OrderStateMediator自动注入到OrderService中,使得订单服务能够方便地调用中介者的协调方法 。同时,库存服务、日志服务和通知服务也通过依赖注入的方式被注入到中介者中,这种方式不仅减少了代码的耦合度,还提高了代码的可测试性 。​

事务控制:在OrderStateMediator的notifyStateChange方法上添加@Transactional注解,确保在处理订单状态变更时,涉及到的库存更新、日志记录和通知发送等多个服务操作要么全部成功,要么全部失败,保证了数据的一致性和完整性 。

当订单状态变更为已支付时,如果库存更新成功,但日志记录失败,整个事务会回滚,库存也会恢复到原来的状态,避免了数据不一致的问题。​

领域事件:通过定义OrderStateChangeEvent领域事件,并结合 Spring 的事件发布和监听机制,可以进一步解耦业务逻辑 。

当订单状态变更时,除了调用相关服务进行库存、日志和通知处理外,还可以发布OrderStateChangeEvent事件,其他感兴趣的服务可以监听该事件并执行相应的业务逻辑,如统计订单状态变更次数、更新订单状态报表等 。这样,即使未来有新的业务需求,也只需要添加新的事件监听器,而不需要修改现有的代码 。​

动态扩展:当系统需要新增协作服务时,比如增加一个积分服务,在用户订单支付成功后为用户增加积分 。只需要在OrderStateMediator中注入积分服务,并在notifyStateChange方法中添加相应的积分增加逻辑即可,无需修改OrderService和其他现有的同事类 。这种设计使得系统具有良好的可扩展性,能够轻松应对业务的变化和发展 。

五、总结 

架构优化价值是中介者模式的一大亮点。它巧妙地将对象间错综复杂的网状依赖梳理成清晰有序的星型结构 。但是中介者模式并非完美无缺,它也存在一些局限性,在实际应用中需要我们谨慎权衡: 

⚠️性能瓶颈是中介者模式在高并发场景下可能面临的问题。因为所有对象之间的交互都要通过中介者来处理,如果中介者的处理能力有限,就会导致消息堆积,响应时间变长 。因此,我们在设计中介者时,需要充分考虑其吞吐量和并发处理能力,可以采用异步处理、缓存等技术来提高其性能 。​

⚠️职责单一问题也不容忽视。如果将过多的业务逻辑集中到中介者中,就会违反单一职责原则,使得中介者变得臃肿和难以维护 。比如在一个企业资源规划(ERP)系统中,如果中介者既要处理订单管理的逻辑,又要处理库存管理、财务管理等多种逻辑,那么中介者的代码会变得非常复杂,难以理解和修改 。为了避免这种情况,应该合理划分中介者的职责,将不同的业务逻辑分配到不同的中介者或模块中,保持中介者的职责单一和高内聚 。​

中介者模式为我们在处理对象间复杂交互方面提供了强大的工具和思路,但设计模式的世界丰富多彩,还有众多其他优秀的设计模式等待我们去探索和学习。在后续的文章中,我们将继续深入剖析各种设计模式~

你可能感兴趣的:(设计模式,设计模式,中介者模式)