53、代码坏味道:中间人 - 实战移除中间人

说实话,中间人这个坏味道,我见过太多次了。

它不像那些一眼就能看出来的大问题,比如几百行的方法、混乱的继承树。中间人很隐蔽,它披着「封装」的外衣,让你觉得「嗯,这样挺合理的」。但时间一长,你就会发现,代码里全是这种「传话筒」——它们不干活,只负责把请求转给真正的对象。

今天我们就来彻底撕掉这层伪装。

什么是中间人?

说白了,就是一个类,它的大部分方法都在调用另一个类的方法。它自己不处理任何业务逻辑,纯粹是「A 找 B,B 再找 C」里的那个 B。

举个例子,你想想看:

public class OrderService {
    private OrderRepository repository;

    public Order findById(Long id) {
        return repository.findById(id);
    }

    public List<Order> findByUserId(Long userId) {
        return repository.findByUserId(userId);
    }

    public void save(Order order) {
        repository.save(order);
    }
}

这个 OrderService 做了什么?什么都没做。它就是把 OrderRepository 的方法又包了一层。调用方本来可以直接用 repository.findById(),现在非要绕一圈经过 service

我在项目中遇到过类似的情况,一个 Controller 层调 Service,Service 调 DAO,DAO 调 JPA Repository。每一层都在做「透传」,没有任何业务逻辑。结果就是,改一个查询条件,要改三层代码。你说累不累?

核心判断标准: 如果一个类中,超过 60% 的方法都是直接委托给另一个类,且没有添加任何额外逻辑,那它就是中间人。

中间人是怎么产生的?

我总结了一下,主要有三个来源:

  1. 过度设计:一开始就想着「我要分层」、「我要解耦」,结果每层都变成了传话筒。
  2. 渐进式腐化:原本这个类是有逻辑的,后来逻辑被抽走了,但委托方法留了下来。
  3. 对封装的误解:觉得「不能让调用方直接访问 Repository」,于是硬加了一层 Service。

嗯,这里要注意第二种情况。我记得有一次重构一个订单模块,发现一个 OrderFacade 类,里面十几个方法全是委托。我查了 Git 历史,发现半年前它确实有逻辑,但后来业务调整,逻辑被移到了别处,委托方法却没人清理。这就是典型的「烂代码沉淀」。

如何识别中间人?

我个人习惯用两个方法:

  • 方法计数法:打开一个类,数一下它自己的方法有几个,委托方法有几个。如果委托方法占大头,报警。
  • 调用链追踪:看调用方是怎么用的。如果调用方每次都要经过三层才能拿到数据,那中间层大概率是多余的。

你还可以用 IDE 的「查找用法」功能,看看这个类被调用的地方。如果调用方都在做同样的事——先调 A,A 再调 B——那为什么不直接调 B 呢?

实战:移除中间人

我们来看一个真实的例子。假设有一个报表系统,代码结构是这样的:

// 中间人类
public class ReportService {
    private ReportDataProvider dataProvider;

    public ReportData getDailyReport(Date date) {
        return dataProvider.fetchDaily(date);
    }

    public ReportData getWeeklyReport(Date start, Date end) {
        return dataProvider.fetchWeekly(start, end);
    }

    public ReportData getMonthlyReport(int year, int month) {
        return dataProvider.fetchMonthly(year, month);
    }
}

// 调用方
public class ReportController {
    private ReportService reportService;

    public void showDailyReport(Date date) {
        ReportData data = reportService.getDailyReport(date);
        // 渲染报表...
    }
}

你看,ReportService 什么都没干,就是转手。调用方 ReportController 完全可以绕过它,直接使用 ReportDataProvider

重构步骤很简单:

  1. 内联方法:把 ReportService 的每个方法,内联到调用方。
  2. 删除中间类:确认没有其他地方使用 ReportService 后,删除它。
  3. 调整依赖:让 ReportController 直接依赖 ReportDataProvider

重构后:

// 调用方直接使用数据提供者
public class ReportController {
    private ReportDataProvider dataProvider;

    public void showDailyReport(Date date) {
        ReportData data = dataProvider.fetchDaily(date);
        // 渲染报表...
    }
}

代码从三层变成了两层,调用链短了,理解成本也降低了。

小技巧: 如果你担心以后会加逻辑,可以先保留中间类,但只保留那些真正有逻辑的方法。纯委托的方法,一律内联掉。等以后需要加逻辑时,再重新提取出来。这叫「延迟决策」——别为未来可能发生的事,提前增加复杂度。

什么时候不该移除中间人?

任何重构手法都有例外。我建议你考虑以下情况:

场景 说明
接口稳定性要求高 如果中间层是公开 API,移除会导致所有调用方修改,那就不动。
未来确定会加逻辑 比如下个迭代就要在中间层加缓存、加权限校验,那可以保留。
跨进程/跨服务调用 如果中间层是 RPC 客户端或消息发送者,它本身就有存在的价值。

我曾经在一个支付项目中,保留了一个看似「中间人」的 PaymentGateway 类。虽然它当时只是委托给第三方 SDK,但我知道下个月要接入第二个支付渠道。如果当时把它内联掉,后面加渠道时就要改所有调用方。嗯,这种「有计划的保留」是可以接受的。

核心逻辑图

下面这张图展示了中间人的识别与移除流程:

中间人识别与移除流程 步骤1:识别中间人 方法计数法 + 调用链追踪 步骤2:判断是否可移除 是否有业务逻辑? 保留中间人 有逻辑,保留 步骤3:移除中间人 内联方法 → 删除类 → 调整依赖 ✅ 调用链缩短,代码更清晰 判断标准:超过60%方法为纯委托 → 中间人

避坑指南

最后,分享几个我踩过的坑:

  • 不要一次性全删:如果中间人有多个调用方,先内联一个方法,跑通测试,再逐步推进。我曾经一次性删掉整个类,结果 CI 挂了半小时。
  • 注意测试覆盖:移除中间人后,测试可能也需要调整。如果中间人的测试只是「验证调用了委托方法」,那这些测试可以直接删掉。
  • 警惕「伪中间人」:有些类虽然方法看起来是委托,但内部做了参数校验、日志、缓存。这种不算中间人,别误删。
警告: 移除中间人后,如果发现调用方代码变得重复(比如每个调用方都写了相同的参数校验),说明你移除错了。这时候应该把公共逻辑提取到真正合适的地方,而不是重新引入中间人。

好了,关于中间人的内容就这些。记住一句话:代码里的传话筒,能少一个是一个

公众号:蓝海资料掘金营,微信 deep3321