在业务系统中,我们常常会遇到一类“历史悠久”的方法:规则繁多、条件复杂、if 语句层层嵌套。虽然功能上能正确运行,但维护成本极高,一旦业务规则需要调整,修改起来如履薄冰,生怕改出新的问题。
下面这段计算产品提成的 Java 代码,就是一个非常典型的例子:
private List<CmsResult> productCommissionCalculate(List<CmsConfigVo> configVos, List<CmsResult> resultList){
for (CmsResult cmsResult : resultList){
BigDecimal newCommission = BigDecimal.ZERO; // 新品提成
BigDecimal oldCommission = BigDecimal.ZERO; // 老品提成
BigDecimal inheritedCommission = BigDecimal.ZERO; // 承接品提成
BigDecimal teamCommission = BigDecimal.ZERO; // 团队提成
// 运营人员
Long operatorUserId = cmsResult.getOperatorUserId();
for (CmsConfigVo configVo : configVos){
if (configVo.getConfigType().equals(CmsProductType.NEW_PROD.getDbValue())) {
// 分割字符串并判断是否包含
if(StringUtils.isNotBlank(configVo.getUserIds())){
String[] userIds = configVo.getUserIds().split(",");
// 需要判断是否匹配该人员
for (String userId : userIds) {
if (StringUtils.isNotBlank(userId) && userId.equals(String.valueOf(operatorUserId))) {
// 判断总销售额在哪个区间内,这里有2种情况:1.区间内 2.区间外(比如:50w以上的getProfitRangeMax的值是0)
// 情况1
if (cmsResult.getTotalSales().compareTo(configVo.getProfitRangeMin()) >= 0 &&
cmsResult.getTotalSales().compareTo(configVo.getProfitRangeMax()) <= 0){
BigDecimal rote = configVo.getCommissionRate().divide(BigDecimal.valueOf(100)).setScale(4, RoundingMode.HALF_UP);
newCommission = cmsResult.getNewProductProfit().multiply(rote);
}
// 情况2
if(cmsResult.getTotalSales().compareTo(configVo.getProfitRangeMin()) > 0 &&
configVo.getProfitRangeMin().compareTo(BigDecimal.ZERO) == 0){
BigDecimal rote = configVo.getCommissionRate().divide(BigDecimal.valueOf(100)).setScale(4, RoundingMode.HALF_UP);
newCommission = cmsResult.getNewProductProfit().multiply(rote);
}
cmsResult.setNewCommission(newCommission);
}
}
}else{
// todo 没有匹配的才会用通用的计算逻辑
}
}
}
}
return resultList;
}
这段代码堪称“教科书式”的坏味道:复杂的业务规则、层层嵌套的 if 判断、字符串解析以及 BigDecimal 的精确计算混杂在一起。虽然当前能跑出正确结果,但从 代码规范 和维护性的角度看,已经到了必须重构的地步。
一、当前代码存在的主要问题
1. 多层嵌套导致认知负担过重
代码的结构可以抽象为:
for (CmsResult cmsResult)
for (CmsConfigVo configVo)
if (configType)
if (userIds)
for (userId)
if (equals)
if (区间1)
if (区间2)
理解这段代码的逻辑,花费在梳理结构上的精力,甚至可能超过了理解业务本身。这种深层嵌套严重违反了单一职责原则,让阅读和维护的成本变得非常高。
2. 区间判断逻辑重复且不直观
原代码中使用了两种条件来判断销售额区间:
// 情况1
if (totalSales >= min && totalSales <= max)
// 情况2
if (totalSales > min && min == 0)
这种分散且带有魔术数字(0)的判断,语义不清晰,非常容易在后续修改中被破坏。业务规则的表达应该集中且自解释。
3. 核心计算逻辑重复
提成的计算逻辑 rate / 100 * profit 在代码中至少出现了两次,并且可以预见,在后续添加老品、承接品提成时,这段相同的代码会被复制粘贴更多次。这种重复是滋生 Bug 和增加维护难度的温床。
二、优化的核心原则
对于这类遗留系统的复杂业务方法,一个稳妥有效的优化原则是:拆解条件、提前返回(或 continue)、方法语义化。通过将复杂的条件判断和具体操作拆分成具有明确命名的小方法,可以大幅提升代码的可读性和可维护性。
三、分步骤实战优化
接下来,我们一步步应用上述原则,对原始代码进行重构。
1. 理顺最外层结构,使用 continue 提前过滤
首先,我们把最外层的循环结构整理清晰,将不符合条件的配置提前跳过。
for (CmsResult cmsResult : resultList) {
BigDecimal newCommission = BigDecimal.ZERO;
// ... 其他提成初始化
Long operatorUserId = cmsResult.getOperatorUserId();
for (CmsConfigVo configVo : configVos) {
// 只处理新品配置
if (!CmsProductType.NEW_PROD.getDbValue().equals(configVo.getConfigType())) {
continue;
}
// 处理专属人员提成
if (hasExclusiveUser(configVo)) {
if (!matchUser(configVo, operatorUserId)) {
continue;
}
if (!inSalesRange(cmsResult.getTotalSales(), configVo)) {
continue;
}
newCommission = calculateCommission(
cmsResult.getNewProductProfit(),
configVo.getCommissionRate()
);
} else {
// todo 通用提成逻辑
}
}
cmsResult.setNewCommission(newCommission);
}
优化后,主循环的逻辑变得一目了然:if-continue 结构清晰地表达了过滤步骤,成功条件汇聚到最后进行计算,结构扁平,易于理解。
2. 抽取“是否有专属人员”的判断
这是一个简单的状态判断,抽取成方法使其意图更明确。
private boolean hasExclusiveUser(CmsConfigVo configVo) {
return StringUtils.isNotBlank(configVo.getUserIds());
}
3. 抽取“用户ID匹配”逻辑
将遍历数组匹配用户ID的逻辑独立出来,消除内层的 for-if 嵌套。
private boolean matchUser(CmsConfigVo configVo, Long operatorUserId) {
String[] userIds = configVo.getUserIds().split(",");
String currentUserId = String.valueOf(operatorUserId);
for (String userId : userIds) {
if (currentUserId.equals(userId)) {
return true;
}
}
return false;
}
如果项目允许使用 Java 8 或更高版本,可以用更简洁的 Stream API 来实现:
return Arrays.stream(configVo.getUserIds().split(","))
.anyMatch(id -> id.equals(String.valueOf(operatorUserId)));
4. 统一封装销售额区间判断(关键步骤)
这是优化中最有价值的一步。我们将分散且晦涩的区间判断逻辑,统一收口到一个语义清晰的方法中。
private boolean inSalesRange(BigDecimal totalSales, CmsConfigVo configVo) {
BigDecimal min = configVo.getProfitRangeMin();
BigDecimal max = configVo.getProfitRangeMax();
// max == 0 表示无上限
if (BigDecimal.ZERO.compareTo(max) == 0) {
return totalSales.compareTo(min) >= 0;
}
return totalSales.compareTo(min) >= 0
&& totalSales.compareTo(max) <= 0;
}
现在,任何关于销售额区间规则的修改,都只需要改动这一个方法,彻底解决了逻辑分散的问题。
5. 统一提成计算方法
将重复的提成计算逻辑提取成通用方法,为后续计算其他类型提成做好准备。
private BigDecimal calculateCommission(BigDecimal profit, BigDecimal rate) {
if (profit == null || rate == null) {
return BigDecimal.ZERO;
}
BigDecimal realRate = rate
.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP);
return profit.multiply(realRate);
}
总结
经过以上五个步骤的重构,原始的“意大利面条式”代码被转化为了结构清晰、职责分明的模块化代码。每个方法只做一件事,并且都有一个名副其实的名字。当业务规则再次变化时,我们只需要修改或替换对应的那个小方法即可,比如修改 inSalesRange 或 calculateCommission,而不会牵一发而动全身。
复杂的业务逻辑本身并不可怕,可怕的是将所有的复杂性都堆砌在一个方法里。通过运用“拆解、提取、语义化”这套组合拳,即使是历史遗留的复杂代码,也能被有效地驯服和优化。这种重构思路,也深刻体现了良好的 设计模式 与编程思想,值得在更多 业务逻辑优化 场景中实践。