if-else 优化的折中思考:不是消灭分支,而是控制风险
前言
各种社区里,你一定见过无数篇标题类似"彻底消灭 if-else"、"告别 if-else 地狱"的文章。这些文章通常会展示一段充满 if-else 的"坏代码",然后用策略模式、工厂模式或者其他设计模式将其"重构"成看起来更优雅的代码。这确实也没啥问题。但是我觉得出发点不太对。不是因为if-else太多我们要重构,而是因为分支耦合在一起,修改一个分支容易影响其他业务,从而产生风险,所以我们需要重构。
if-else 不是问题,它只是表象。我们重构代码的目的不是为了消灭 if-else,而是为了控制风险。
比如我们项目中的会员价格服务
核心业务逻辑如下:
scss
@Override
public void memberPriceAddOrModify(MemberPriceAddOrModifyParam param) {
LogUtil.info(log, "memberPriceAddOrModify >> 商品会员价新增和修改统一服务开始 >> param = {}", param);
// 参数校验
ValidateUtil.validateWithThrow(param);
// 获取goodsIdList
List<String> goodsIdList = param.getGoodsPriceList().stream()
.map(GoodsPriceInfoParam::getGoodsId)
.collect(Collectors.toList());
goodsIdList = goodsIdList.stream().distinct().collect(Collectors.toList());
if (goodsIdList.size() == 0) {
LogUtil.info(log, "memberPriceAddOrModify >> 商品会员价新增和修改统一服务结束 >> param = {}", param);
return;
}
// 判断商品档案/门店商品/网店商品类型
if (param.getBelong().equals(BelongEnum.MERCHANT.getValue())) {
dealUserGoodsPriceExt(goodsIdList, param);
} else if (param.getBelong().equals(BelongEnum.STORE.getValue())) {
dealStoreGoodsPriceExt(goodsIdList, param);
} else if (param.getBelong().equals(BelongEnum.ONLINE_STORE.getValue())) {
dealOnlineGoodsPriceExt(goodsIdList, param);
}
LogUtil.info(log, "memberPriceAddOrModify >> 商品会员价新增和修改统一服务结束 >> param = {}", param);
}
第一眼看上去,这段代码有明显的 if-else 链。按照传统的"消灭 if-else"思路,我们应该考虑引入策略模式,去优雅的实现。
但仔细想想if-else有问题吗? 每个分支也通过函数进行隔离,就是七八个又能怎么样? 也不会有太多的理解成本。那为啥要重构?
可预见的风险点:
- 业务影响面不可控:三个分支的业务逻辑混在一个类中,修改任何一个分支都可能影响其他业务
- 代码重复风险:三个方法(dealUserGoodsPriceExt、dealStoreGoodsPriceExt、dealOnlineGoodsPriceExt)的逻辑高度相似,只是操作的 DAO 和 DO 对象不同
这些分支的修改频率如何?
让我们思考一个真实场景:
- 商户商品(MERCHANT)的价格逻辑需要支持新的促销规则
- 门店商品(STORE)的价格逻辑需要对接新的库存系统
- 网店商品(ONLINE_STORE)的价格逻辑需要支持预售价格
问题来了 :如果这三个分支的逻辑都在 PriceServiceImpl
这一个类中,那么:
- 修改商户商品的逻辑,可能不小心影响到门店商品
- 三个需求同时开发时,会频繁产生代码冲突
- 测试时很难确保某个分支的修改不影响其他分支
所以真正的风险不在于有 if-else,而在于:
- 业务边界不清晰:三个独立的业务逻辑耦合在一起
- 修改影响面不可控:改一个分支可能影响其他分支
- 代码重复:增加了维护成本
隔离业务影响面
所以很顺畅的就能想到。我们不是为了消灭 if-else,而是为了让每个分支的修改只影响自己的业务。
将业务逻辑隔离到独立的类中
真正的"隔离"不是提取一个公共方法,而是将不同业务的代码物理隔离:
typescript
// 商户商品价格服务 - 独立的类
@Service
public class MerchantGoodsPriceService {
@Autowired
private UserGoodsPriceExtDAO userGoodsPriceExtDAO;
/**
* 处理商户商品价格
* 这个类只负责商户商品,修改不会影响门店和网店
*/
public void handleMemberPrice(List<String> goodsIdList, MemberPriceAddOrModifyParam param) {
TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
userGoodsPriceExtDAO.delByGoodsIdListAndPriceType(param.getGsUid(), param.getPriceType(), goodsIdList);
param.getGoodsPriceList().forEach(info -> {
// ... 插入逻辑 ...
});
transactionManager.commit(transaction);
} catch (Exception e) {
LogUtil.error(log, "商户商品价格处理异常", e);
transactionManager.rollback(transaction);
throw e;
}
}
}
// 门店商品价格服务 - 独立的类
@Service
public class StoreGoodsPriceService {
@Autowired
private StoreGoodsPriceExtDAO storeGoodsPriceExtDAO;
/**
* 处理门店商品价格
* 这个类只负责门店商品,可以独立演化
*/
public void handleMemberPrice(List<String> goodsIdList, MemberPriceAddOrModifyParam param) {
// 类似实现,但可以独立修改
}
}
// 网店商品价格服务 - 独立的类
@Service
public class OnlineGoodsPriceService {
@Autowired
private OnlineGoodsPriceExtDAO onlineGoodsPriceExtDAO;
/**
* 处理网店商品价格
* 这个类只负责网店商品,可以有自己的特殊逻辑
*/
public void handleMemberPrice(List<String> goodsIdList, MemberPriceAddOrModifyParam param) {
// 类似实现,但可以独立修改
}
}
好处:
- 修改影响面可控 :修改商户商品逻辑时,只会修改
MerchantGoodsPriceService
,不会影响门店和网店 - 避免代码冲突:三个团队可以同时修改各自的服务类,不会产生 Git 冲突
- 测试更简单:可以单独测试每个服务类,不用担心影响其他业务
- 独立演化:未来商户商品可能支持分级定价,门店商品可能对接库存系统,各自可以独立演化
消除代码重复(可选)
虽然我们已经将业务隔离到不同的类中,但你会发现三个类中的代码逻辑高度相似。这时候需要思考:这是偶然重复还是本质重复?
判断标准:
- 本质重复:如果未来三个业务的处理流程会一直保持一致(删除 -> 插入),那就应该提取公共逻辑
- 偶然重复:如果未来商户商品可能要加审核流程、门店商品要对接库存、网店商品要支持预售,那就不应该强行合并
假设这是本质重复,我们可以提取一个抽象基类:
scss
// 抽象基类 - 定义通用流程
public abstract class AbstractGoodsPriceService<T> {
protected abstract DAO<T> getDAO();
protected abstract T buildPriceDO(GoodsPriceInfo info, PriceInfo priceInfo, MemberPriceAddOrModifyParam param);
/**
* 通用的价格处理流程
*/
public void handleMemberPrice(List<String> goodsIdList, MemberPriceAddOrModifyParam param) {
TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 删除旧数据
getDAO().delByGoodsIdListAndPriceType(param.getGsUid(), param.getPriceType(), goodsIdList);
// 批量插入新数据
List<T> dataList = new ArrayList<>();
param.getGoodsPriceList().forEach(info -> {
if (StrUtil.isNotBlank(info.getGoodsId()) &&
CollectionUtil.isNotEmpty(info.getPriceInfoParamList())) {
info.getPriceInfoParamList().forEach(priceInfo -> {
if (priceInfo.getPrice() != null &&
priceInfo.getPrice().compareTo(BigDecimal.ZERO) > 0) {
dataList.add(buildPriceDO(info, priceInfo, param));
}
});
}
});
if (CollectionUtil.isNotEmpty(dataList)) {
getDAO().batchInsert(dataList);
}
transactionManager.commit(transaction);
} catch (Exception e) {
LogUtil.error(log, "价格处理异常", e);
transactionManager.rollback(transaction);
throw e;
}
}
}
// 商户商品价格服务 - 只需实现差异部分
@Service
public class MerchantGoodsPriceService extends AbstractGoodsPriceService<UserGoodsPriceExtDO> {
@Autowired
private UserGoodsPriceExtDAO userGoodsPriceExtDAO;
@Override
protected DAO<UserGoodsPriceExtDO> getDAO() {
return userGoodsPriceExtDAO;
}
@Override
protected UserGoodsPriceExtDO buildPriceDO(GoodsPriceInfo info, PriceInfo priceInfo, MemberPriceAddOrModifyParam param) {
UserGoodsPriceExtDO userGoodsPriceExtDO = new UserGoodsPriceExtDO();
userGoodsPriceExtDO.setGsUid(param.getGsUid());
userGoodsPriceExtDO.setPriceType(param.getPriceType());
userGoodsPriceExtDO.setPrice(priceInfo.getPrice());
userGoodsPriceExtDO.setUserGoodsId(info.getGoodsId());
userGoodsPriceExtDO.setRightConfigNo(priceInfo.getRightNo());
userGoodsPriceExtDO.setDiscountType(priceInfo.getDiscountType());
return userGoodsPriceExtDO;
}
}
但要注意:
- 如果未来三个业务会朝不同方向演化,不要提取公共逻辑,保持代码重复反而更安全
- 提取公共逻辑的前提是:这些业务的核心流程在未来很长时间内都会保持一致
可读性 vs 开闭原则 - 业务开发中的折中选择
到这一步,其实我们已经完成了最重要的优化:业务影响面隔离 。现在才真正需要思考:是否需要消除 if-else?
这个问题的本质是:在可读性、开闭原则和设计模式之间如何折中?
方案一:保留 if-else(优先可读性)
scss
@Service
public class PriceServiceImpl implements PriceService {
@Autowired
private MerchantGoodsPriceService merchantGoodsPriceService;
@Autowired
private StoreGoodsPriceService storeGoodsPriceService;
@Autowired
private OnlineGoodsPriceService onlineGoodsPriceService;
@Override
public void memberPriceAddOrModify(MemberPriceAddOrModifyParam param) {
// ... 参数校验 ...
// 清晰的路由逻辑:一眼就能看出系统支持哪几种商品类型
if (param.getBelong().equals(BelongEnum.MERCHANT.getValue())) {
merchantGoodsPriceService.handleMemberPrice(goodsIdList, param);
} else if (param.getBelong().equals(BelongEnum.STORE.getValue())) {
storeGoodsPriceService.handleMemberPrice(goodsIdList, param);
} else if (param.getBelong().equals(BelongEnum.ONLINE_STORE.getValue())) {
onlineGoodsPriceService.handleMemberPrice(goodsIdList, param);
} else {
throw new IllegalArgumentException("不支持的商品归属类型: " + param.getBelong());
}
}
}
-
可读性极佳
- 一眼就能看出系统支持哪三种商品类型
- 新人不需要理解策略模式等概念
- 代码即文档,维护成本低
-
修改成本可控
- 虽然违反了"开闭原则"(新增类型需要修改这个类)
- 但修改很简单:加一个 else if 分支,5 秒钟完成
- 业务已经隔离,不会影响其他分支
-
符合业务现实
- 商户商品、门店商品、网店商品是业务核心概念,非常稳定
- 未来新增商品类型的概率极低(可能 1-2 年才新增一次)
- 即使新增,修改这个类也不是什么大问题
方案二:引入策略模式(遵循开闭原则)
java
@Service
public class PriceServiceImpl implements PriceService {
private final Map<BelongEnum, PriceHandler> handlerMap;
public PriceServiceImpl(List<PriceHandler> handlers) {
// 通过 Spring 自动注入所有 PriceHandler 实现
this.handlerMap = handlers.stream()
.collect(Collectors.toMap(PriceHandler::supportedBelong, Function.identity()));
}
@Override
public void memberPriceAddOrModify(MemberPriceAddOrModifyParam param) {
// ... 参数校验 ...
PriceHandler handler = handlerMap.get(BelongEnum.valueOf(param.getBelong()));
if (handler == null) {
throw new IllegalArgumentException("不支持的商品归属类型: " + param.getBelong());
}
handler.handleMemberPrice(goodsIdList, param);
}
}
优势分析:
-
符合开闭原则
- 新增商品类型时,只需要新增一个
PriceHandler
实现类 - 不需要修改
PriceServiceImpl
- 听起来很美好...
- 新增商品类型时,只需要新增一个
-
支持扩展
- 如果未来要支持插件化、第三方扩展,这个架构是基础
- 如果商品类型会频繁新增,这个方案更合适
但代价是什么?
-
可读性变差
- 看不出系统支持哪几种商品类型,需要全局搜索
PriceHandler
的实现类 - 新人需要理解:Spring 的 List 注入、Stream API、策略模式、Map 映射...
- Debug 时需要跳转多个类才能找到实际的处理逻辑
- 看不出系统支持哪几种商品类型,需要全局搜索
-
过度抽象
- 为了 3 个固定的类型引入接口和 Map,投入产出比低
- 增加了系统复杂度,但实际收益很小
-
开闭原则的滥用
-
在业务开发中,修改原有类是常态,不是什么罪过
-
新增商品类型时,除了加新类,你还要:
- 修改枚举
BelongEnum
(不可避免) - 修改数据库表设计(不可避免)
- 修改前端页面(不可避免)
- 修改配置文件(不可避免)
- 修改枚举
-
那为什么唯独不能修改
PriceServiceImpl
呢?
-
在业务开发中,完全遵循开闭原则往往是一种过度设计。开闭原则的价值在于应对频繁变化,而不是僵化地避免修改代码。"
很多时候可读性优先,适度设计
-
可读性是第一生产力
- 代码被阅读的次数远超被修改的次数
- 一个清晰的 if-else 胜过一个需要跳转 5 个文件才能理解的策略模式
- 团队成员的理解成本 > 偶尔修改一次的成本
-
业务开发避免不了修改原有类
- 新增功能时,修改相关的类是正常的,不是什么罪过
- 重要的是控制修改的影响面(我们已经通过隔离做到了)
- 而不是教条式地遵循开闭原则
-
投入产出比
- 引入策略模式的成本:1 个接口 + 修改 3 个服务类 + 修改入口逻辑 + 团队学习成本
- 收益:新增类型时少改一个 if-else 分支(但枚举、数据库、前端还是要改)
- 这个投入产出比太低了
-
YAGNI 原则(You Aren't Gonna Need It)
- 不要为了"未来可能需要扩展"而过度设计
- 等真正需要频繁扩展时,再重构也不迟
- 过早优化是万恶之源
总结
回到文章开头的观点:重构代码不是为了消灭 if-else,而是为了控制风险。
一个好的重构流程应该是:
-
识别风险:每个分支的修改是否会影响其他业务?分支内部是否有复杂的业务逻辑?
-
隔离业务影响面:将不同的业务逻辑隔离到独立的类中,让每个分支的修改只影响自己
- 这是最重要的一步,比消灭 if-else 重要得多
- 隔离后,即使保留 if-else,代码也是可控的
-
谨慎消除重复:判断代码重复是本质重复还是偶然重复
- 本质重复:提取公共逻辑
- 偶然重复:保持重复,允许独立演化
-
评估扩展性:未来是否会频繁新增分支?
- 会扩展:考虑引入策略模式
- 不会扩展:保留 if-else,不要过度设计
思考的时候要明白:
-
隔离比消除更重要
- if-else 不是敌人,业务耦合才是
- 将分支隔离到独立的类中,比用策略模式消除 if-else 更有价值
- 隔离后的 if-else 只是路由逻辑,很清晰,不需要优化
-
简单比优雅更重要
- 3 个分支的 if-else 比策略模式更容易理解
- 不要为了遵循"开闭原则"而引入不必要的抽象
- 当业务确实需要扩展时,再引入策略模式也不迟
-
风险可控比代码优雅更重要
- 允许偶然重复,比强行合并导致未来修改相互影响更安全
- 保留简单的 if-else,比过度抽象导致代码难以理解更好
作为普通的程序员,我们的首要任务是让代码的修改影响面可控,而不是写出"看起来很高级"的代码。在实际工作中:
- 一个业务隔离清晰的 if-else,胜过一个过度设计的策略模式
- 一个事务边界明确的编程式事务,胜过一个可能失效的声明式事务
- 一段重复但独立演化的代码,胜过一个强行合并的公共方法
叠甲叠甲: 我不是反对把if-else重构成策略模式,这完全没问题,是个人的选择。我只是if-else分支太多不是我们重构代码的核心理由,通过消解ifelse来控制风险才是原因。