数据泥团:当代码里到处是“三件套”
说实话,我见过太多项目里藏着这样的代码:三个、四个甚至五个字段,像连体婴儿一样,在方法参数、类字段、临时变量里反复出现。比如一个订单系统里,startDate、endDate、status 这三个东西,几乎每个查询方法都要带上。这就是典型的数据泥团。
数据泥团本身不是 bug,但它是个强烈的信号——你的代码在喊:“快把我提炼成一个类!” 今天我们就来聊聊怎么收拾它。
什么是数据泥团?
简单说,就是一组数据项总是一起出现。你想想看,如果三个字段永远同时作为参数传递,或者同时出现在多个类的字段列表里,那它们大概率属于同一个概念。
举个例子:
// 泥团:三个参数总是一起出现
public List<Order> queryOrders(String startDate, String endDate, String status) {
// ...
}
public List<Order> queryRefunds(String startDate, String endDate, String status) {
// ...
}
public long countOrders(String startDate, String endDate, String status) {
// ...
}
这三个方法,参数列表完全一样。每次调用都要写三遍同样的东西。改个参数名?三个地方都得改。这就是泥团。
怎么识别?三个信号
我个人习惯用这三个标准来判断:
- 重复出现:同一组数据出现在三个或更多方法签名里。
- 一起变化:新增一个字段时,所有用到这组数据的地方都要改。
- 有共同行为:这组数据经常被一起校验、格式化或计算。
核心判断标准:如果这组数据能用一个名词概括,那它就该是一个类。
实战:提炼类
好,我们直接动手。还是上面那个订单查询的例子。第一步,把泥团提炼成一个类:
public class OrderQueryCriteria {
private String startDate;
private String endDate;
private String status;
public OrderQueryCriteria(String startDate, String endDate, String status) {
this.startDate = startDate;
this.endDate = endDate;
this.status = status;
}
// getters...
}
第二步,把方法签名改成接收这个对象:
public List<Order> queryOrders(OrderQueryCriteria criteria) {
// 使用 criteria.getStartDate() 等
}
public List<Order> queryRefunds(OrderQueryCriteria criteria) {
// ...
}
public long countOrders(OrderQueryCriteria criteria) {
// ...
}
你看,参数列表从三个缩成了一个。调用方也清爽了:
// 之前
orderService.queryOrders("2024-01-01", "2024-12-31", "PAID");
// 之后
OrderQueryCriteria criteria = new OrderQueryCriteria("2024-01-01", "2024-12-31", "PAID");
orderService.queryOrders(criteria);
小技巧:如果调用方经常要构造这个对象,可以加个静态工厂方法或者 Builder 模式。我个人偏好 Builder,尤其是字段超过三个的时候。
引入参数对象:不止是打包
提炼类只是第一步。真正的价值在于,你可以把行为也搬进去。这就是“引入参数对象”的精髓。
比如,校验逻辑:
// 之前:校验散落在各处
public void validateQuery(String startDate, String endDate, String status) {
if (startDate == null || endDate == null) {
throw new IllegalArgumentException("日期不能为空");
}
if (startDate.compareTo(endDate) > 0) {
throw new IllegalArgumentException("开始日期不能晚于结束日期");
}
}
// 之后:校验搬进类里
public class OrderQueryCriteria {
private String startDate;
private String endDate;
private String status;
public OrderQueryCriteria(String startDate, String endDate, String status) {
validate(startDate, endDate);
this.startDate = startDate;
this.endDate = endDate;
this.status = status;
}
private void validate(String start, String end) {
if (start == null || end == null) {
throw new IllegalArgumentException("日期不能为空");
}
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException("开始日期不能晚于结束日期");
}
}
public boolean isActive() {
return "ACTIVE".equals(status);
}
}
嗯,这里要注意:isActive() 这种业务判断,以前可能散落在各个 service 方法里。现在直接问 criteria 就行了。这就是面向对象——数据和操作在一起。
避坑指南
我曾经踩过一个坑:把不相关的数据硬塞进一个类里。比如把用户 ID 和订单日期放在一起,只因为它们同时出现在一个查询方法里。结果后来需求变了,用户 ID 单独要用,订单日期也要单独用,那个类反而成了累赘。
所以记住:数据泥团提炼出来的类,必须代表一个真实的业务概念。不是随便把几个参数打包就完事了。
警告:不要为了“减少参数个数”而强行合并。如果一组数据没有共同的行为和变化原因,它们只是碰巧出现在一起,不是泥团。
什么时候不做?
也不是所有泥团都要提炼。我个人习惯看这几个条件:
- 出现次数:少于 3 次,先不动。等它再出现一次再说。
- 变化频率:如果这组数据几乎不变,而且只有两个地方用,保持原样也行。
- 团队习惯:如果团队里大家都习惯用基本类型传参,突然引入一个类反而增加理解成本。这时候可以先做小范围试点。
核心逻辑图
下面这张图总结了数据泥团的识别、处理和收益:
总结
数据泥团这个坏味道,说白了就是代码在告诉你:“这些数据应该是一个对象。” 别忽视它。提炼类 + 引入参数对象,这两步做完,你的代码会变得更清晰、更安全、更容易测试。
我个人习惯在 code review 时特别留意参数列表超过三个的方法。一旦发现,我就会问自己:这些参数是不是一个泥团?如果是,那就动手。别等到泥团长成巨石再处理——那时候改起来就疼了。
记住:好的代码不是写出来的,是改出来的。每次看到泥团,就是一次重构的机会。