你在 Controller 里注入 8 个 Service,其实是想请一个中介者

有个项目我接手的时候,Controller 层长这样:

java 复制代码
@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired private OrderService orderService;
    @Autowired private InventoryService inventoryService;
    @Autowired private PaymentService paymentService;
    @Autowired private NotificationService notificationService;
    @Autowired private LogisticsService logisticsService;
    @Autowired private CouponService couponService;
    @Autowired private UserService userService;
    @Autowired private AuditService auditService;

    @PostMapping("/submit")
    public Result submit(@RequestBody OrderDTO dto) {
        // 每个 Service 之间还要传来传去
        User user = userService.getById(dto.getUserId());
        Coupon coupon = couponService.validate(dto.getCouponId(), user);
        boolean hasStock = inventoryService.checkStock(dto.getItems());
        if (!hasStock) return Result.fail("库存不足");
        Order order = orderService.create(dto, user, coupon);
        inventoryService.lock(dto.getItems());
        PaymentResult pay = paymentService.pay(order, coupon);
        notificationService.sendOrderConfirm(user, order);
        logisticsService.createShipment(order);
        auditService.record("ORDER_SUBMIT", user.getId(), order.getId());
        return Result.ok(order);
    }
}

八个 Service 注入,十几个调用步骤。每次产品加一个新流程------比如"下单成功后要发优惠券"------就得在这个 Controller 里再加一行。三个月后,这个方法长到了 200 行,没人敢动,也没人看得懂。

这就是典型的中介者模式缺失症

不是代码写得烂,是架构少了一层

中介者模式(Mediator Pattern)解决的核心问题一句话就能说清楚:一堆对象互相通信导致网状依赖,引入一个中介者把它们变成星形依赖。

说人话:把 A 调 B、B 调 C、C 调 D 的乱麻,变成大家都只跟一个 M 聊,M 负责协调所有人。

很多工程师觉得中介者模式"太简单了没啥用"或者"就是个 EventBus"。但实际上你在日常写代码时已经反复遇到需要中介者的场景,只是你没意识到这是个设计模式问题。

聊天室:中介者最直观的教科书案例

假设你写一个聊天室,没有中介者的话:

java 复制代码
public class User {
    private String name;
    private List<User> contacts; // 每个用户持有所有联系人

    public void send(String msg) {
        for (User contact : contacts) {
            contact.receive(name, msg);
        }
    }
}

每个 User 都要知道所有其他 User,加人删人都得遍历更新。更恶心的是,如果你想加一个"敏感词过滤"功能,得在每个 User 的 send 方法里加------或者更糟,你得让 User 去依赖一个 Filter 类。

引入中介者:

java 复制代码
public interface ChatMediator {
    void sendMessage(User sender, String msg);
    void addUser(User user);
}

public class ChatRoom implements ChatMediator {
    private List<User> users = new ArrayList<>();
    private SensitiveWordFilter filter = new SensitiveWordFilter();

    public void sendMessage(User sender, String msg) {
        String filtered = filter.check(msg);
        for (User user : users) {
            if (user != sender) {
                user.receive(sender.getName(), filtered);
            }
        }
    }
}

public class User {
    private ChatMediator mediator; // 只认识中介者
    private String name;

    public void send(String msg) {
        mediator.sendMessage(this, msg);
    }
}

User 从认识所有人变成只认识 ChatRoom。加过滤、加日志、加持久化,全在 ChatRoom 里改,User 一行不动。

这就是中介者模式的价值:把 N×N 的依赖关系压缩成 N×1。

微服务编排:中介者模式的分布式版本

回到开头那个下单 Controller 的问题------那其实是一个微服务编排问题在单体里的预习。

在分布式系统里,下单流程涉及七八个服务,你有两种选择:

编排(Orchestration)------一个中心化的编排器协调所有服务。这就是中介者。

编排(Choreography)------每个服务自己监听事件、自己决定做什么。这是观察者。

两种方式没有绝对的好坏,但中介者(编排)有一个观察者没有的优势:你能在一个地方看到完整的业务流程。

java 复制代码
// 编排器 = 中介者
public class OrderSagaOrchestrator {
    
    public OrderResult submitOrder(OrderDTO dto) {
        // Step 1: 验证用户
        User user = userService.getById(dto.getUserId());
        if (user == null) return fail("用户不存在");

        // Step 2: 验证优惠券
        if (dto.getCouponId() != null) {
            CouponResult cr = couponService.validate(dto.getCouponId(), user.getId());
            if (!cr.isValid()) return fail(cr.getReason());
        }

        // Step 3: 检查库存
        InventoryResult ir = inventoryService.checkAndLock(dto.getItems());
        if (!ir.isSuccess()) return fail("库存不足");

        // Step 4: 创建订单
        Order order = orderService.create(dto);

        // Step 5: 支付
        PaymentResult pr = paymentService.execute(order);
        if (!pr.isSuccess()) {
            // 补偿:释放库存
            inventoryService.unlock(dto.getItems());
            return fail("支付失败");
        }

        // Step 6: 后续操作
        notificationService.sendConfirm(user, order);
        logisticsService.createShipment(order);
        couponService.markUsed(dto.getCouponId());
        
        return Result.ok(order);
    }
}

Orchestrator 把所有步骤集中管理,补偿逻辑写在一处,业务流程一目了然。

但这里有一个中介者模式的经典陷阱:中介者本身成了上帝类。

中介者不是上帝类:分层的艺术

直接用一个 Orchestrator 接管所有流程,三个月后这个类 500 行,也会变成新的噩梦。

解决思路是按领域拆分中介者

java 复制代码
// 不要让一个 Mediator 管所有事
public class OrderSubmitMediator {
    private UserValidator userValidator;
    private CouponValidator couponValidator;
    private InventoryCoordinator inventoryCoordinator;
    private PaymentCoordinator paymentCoordinator;
    
    public Result submit(OrderDTO dto) {
        // 每个子协调器处理自己的领域
        userValidator.validate(dto.getUserId());
        couponValidator.validate(dto.getCouponId(), dto.getUserId());
        inventoryCoordinator.lock(dto.getItems());
        // ...
    }
}

把大中介者拆成多个小中介者,每个管一个子领域。这就是"中介者的中介者"------一种递归的组合。

另一个工程上的解法是用事件总线代替中介者:

java 复制代码
public class OrderEventBus {
    private Map<Class<?>, List<Consumer<Object>>> handlers = new HashMap<>();
    
    public <T> void register(Class<T> eventType, Consumer<T> handler) {
        handlers.computeIfAbsent(eventType, k -> new ArrayList<>())
                .add((Consumer<Object>) handler);
    }
    
    public void publish(Object event) {
        List<Consumer<Object>> consumers = handlers.get(event.getClass());
        if (consumers != null) {
            consumers.forEach(c -> c.accept(event));
        }
    }
}

// 使用
eventBus.register(OrderCreatedEvent.class, e -> {
    notificationService.sendConfirm(e.getUser(), e.getOrder());
});
eventBus.register(OrderCreatedEvent.class, e -> {
    logisticsService.createShipment(e.getOrder());
});

// Controller 只发布事件
orderService.create(dto);
eventBus.publish(new OrderCreatedEvent(order, user));

但事件总线的缺点是:你失去了流程的可见性。哪些 handler 会响应 OrderCreatedEvent?handler 之间的执行顺序是什么?这又回到了编排 vs 编排的经典取舍。

中介者给你流程可见性,事件总线给你解耦灵活性。选哪个取决于你的场景更需要哪一样。

Spring 里其实到处都是中介者

DispatcherServlet 某种程度上就是一个中介者------它自己不处理业务,但它协调 HandlerMapping、HandlerAdapter、ViewResolver 之间的交互。

java 复制代码
// DispatcherServlet 的核心方法(简化)
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    // 1. 根据 request 找 Handler
    HandlerExecutionChain handler = getHandler(request);
    // 2. 根据 Handler 找适配器
    HandlerAdapter ha = getHandlerAdapter(handler.getHandler());
    // 3. 执行前置拦截器
    if (!handler.applyPreHandle(request, response)) return;
    // 4. 实际调用 Handler
    ModelAndView mv = ha.handle(request, response, handler.getHandler());
    // 5. 执行后置拦截器
    handler.applyPostHandle(request, response, mv);
    // 6. 解析视图
    processDispatchResult(request, response, handler, mv);
}

这里面 HandlerMapping、HandlerAdapter、ViewResolver 彼此不直接通信,全由 DispatcherServlet 这个中介者协调。这就是典型的中介者模式。

你不需要实现一个 ChatRoom 才算用了中介者------任何把多对多调用收敛到一对多调用的设计,都是中介者。

什么时候不该用中介者

说一个反直觉的事:不是所有网状依赖都需要中介者。

如果你的业务逻辑本身就很简单------比如只有两三个对象的协作------引入中介者反而增加了一层无意义的抽象。你本来 A→B→C 调用链很清晰,加个 Mediator 变成 A→M→B→M→C,写代码的人得多绕一圈。

另外,如果你的"对象之间的通信"本质上是数据流而不是控制流------比如一个数据管道 ETL 的场景------那更适合用管道-过滤器模式,而不是中介者。

回到那个 8 个 Service 的 Controller

我用中介者模式重构后,Controller 变成了:

java 复制代码
@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired private OrderSubmitMediator mediator;

    @PostMapping("/submit")
    public Result submit(@RequestBody OrderDTO dto) {
        return mediator.submit(dto);
    }
}

Controller 只有一行。业务流程、补偿逻辑、异常处理全部在 Mediator 里。加了新流程也只需改 Mediator 或其子协调器。

代码行数没少------甚至可能多了------但每个类的职责清晰了。这就是设计模式的价值:不是为了少写代码,是为了让代码能被读懂、能被改动、能被测试。

对了,我正在做一个用卡皮巴拉讲设计模式的微信小程序「爪爪代码冒险记」,23 个设计模式用漫画故事 + 答题闯关的方式讲,中介者模式在里面的故事是一群动物吵架让卡皮巴拉来调解,如果你感兴趣可以搜一下,或者等我后面的文章,每个模式我都会同步对应的漫画内容。

相关推荐
AskHarries1 小时前
Shell Tool:命令执行、输出读取和长任务管理
后端
小闹5491 小时前
Docker 如何才能学的更扎实
后端·程序员
武子康1 小时前
Java-28 深入浅出 Spring 实现简易Ioc-04 在上节的业务下手动实现AOP
java·后端·mybatis
XovH2 小时前
MySQL 系列:第10篇 存储过程与自定义函数
后端
XovH2 小时前
MySQL 系列:第8篇 子查询与集合操作
后端
XovH2 小时前
MySQL 系列:第9篇 视图——定制化数据窗口
后端
vortex52 小时前
新手前后端开发学习指南:从Flask框架到全栈实践
后端·python·flask
leeyi2 小时前
Retriever 组件:让 Agent 学会「翻资料」的统一接口
人工智能·后端·agent
一个做软件开发的牛马2 小时前
MyBatis 从零实战:完整搭建可运行 Demo,注解与 XML 双模式开发详解
java·后端