if-else 优化的折中思考:不是消灭分支,而是控制风险

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有问题吗? 每个分支也通过函数进行隔离,就是七八个又能怎么样? 也不会有太多的理解成本。那为啥要重构?

可预见的风险点

  1. 业务影响面不可控:三个分支的业务逻辑混在一个类中,修改任何一个分支都可能影响其他业务
  2. 代码重复风险:三个方法(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) {
        // 类似实现,但可以独立修改
    }
}

好处

  1. 修改影响面可控 :修改商户商品逻辑时,只会修改 MerchantGoodsPriceService,不会影响门店和网店
  2. 避免代码冲突:三个团队可以同时修改各自的服务类,不会产生 Git 冲突
  3. 测试更简单:可以单独测试每个服务类,不用担心影响其他业务
  4. 独立演化:未来商户商品可能支持分级定价,门店商品可能对接库存系统,各自可以独立演化

消除代码重复(可选)

虽然我们已经将业务隔离到不同的类中,但你会发现三个类中的代码逻辑高度相似。这时候需要思考:这是偶然重复还是本质重复?

判断标准

  • 本质重复:如果未来三个业务的处理流程会一直保持一致(删除 -> 插入),那就应该提取公共逻辑
  • 偶然重复:如果未来商户商品可能要加审核流程、门店商品要对接库存、网店商品要支持预售,那就不应该强行合并

假设这是本质重复,我们可以提取一个抽象基类:

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());
        }
    }
}
  1. 可读性极佳

    • 一眼就能看出系统支持哪三种商品类型
    • 新人不需要理解策略模式等概念
    • 代码即文档,维护成本低
  2. 修改成本可控

    • 虽然违反了"开闭原则"(新增类型需要修改这个类)
    • 但修改很简单:加一个 else if 分支,5 秒钟完成
    • 业务已经隔离,不会影响其他分支
  3. 符合业务现实

    • 商户商品、门店商品、网店商品是业务核心概念,非常稳定
    • 未来新增商品类型的概率极低(可能 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);
    }
}

优势分析

  1. 符合开闭原则

    • 新增商品类型时,只需要新增一个 PriceHandler 实现类
    • 不需要修改 PriceServiceImpl
    • 听起来很美好...
  2. 支持扩展

    • 如果未来要支持插件化、第三方扩展,这个架构是基础
    • 如果商品类型会频繁新增,这个方案更合适

但代价是什么?

  1. 可读性变差

    • 看不出系统支持哪几种商品类型,需要全局搜索 PriceHandler 的实现类
    • 新人需要理解:Spring 的 List 注入、Stream API、策略模式、Map 映射...
    • Debug 时需要跳转多个类才能找到实际的处理逻辑
  2. 过度抽象

    • 为了 3 个固定的类型引入接口和 Map,投入产出比低
    • 增加了系统复杂度,但实际收益很小
  3. 开闭原则的滥用

    • 在业务开发中,修改原有类是常态,不是什么罪过

    • 新增商品类型时,除了加新类,你还要:

      • 修改枚举 BelongEnum(不可避免)
      • 修改数据库表设计(不可避免)
      • 修改前端页面(不可避免)
      • 修改配置文件(不可避免)
    • 那为什么唯独不能修改 PriceServiceImpl 呢?

在业务开发中,完全遵循开闭原则往往是一种过度设计。开闭原则的价值在于应对频繁变化,而不是僵化地避免修改代码。"

很多时候可读性优先,适度设计

  1. 可读性是第一生产力

    • 代码被阅读的次数远超被修改的次数
    • 一个清晰的 if-else 胜过一个需要跳转 5 个文件才能理解的策略模式
    • 团队成员的理解成本 > 偶尔修改一次的成本
  2. 业务开发避免不了修改原有类

    • 新增功能时,修改相关的类是正常的,不是什么罪过
    • 重要的是控制修改的影响面(我们已经通过隔离做到了)
    • 而不是教条式地遵循开闭原则
  3. 投入产出比

    • 引入策略模式的成本:1 个接口 + 修改 3 个服务类 + 修改入口逻辑 + 团队学习成本
    • 收益:新增类型时少改一个 if-else 分支(但枚举、数据库、前端还是要改)
    • 这个投入产出比太低了
  4. YAGNI 原则(You Aren't Gonna Need It)

    • 不要为了"未来可能需要扩展"而过度设计
    • 等真正需要频繁扩展时,再重构也不迟
    • 过早优化是万恶之源

总结

回到文章开头的观点:重构代码不是为了消灭 if-else,而是为了控制风险。

一个好的重构流程应该是:

  1. 识别风险:每个分支的修改是否会影响其他业务?分支内部是否有复杂的业务逻辑?

  2. 隔离业务影响面:将不同的业务逻辑隔离到独立的类中,让每个分支的修改只影响自己

    • 这是最重要的一步,比消灭 if-else 重要得多
    • 隔离后,即使保留 if-else,代码也是可控的
  3. 谨慎消除重复:判断代码重复是本质重复还是偶然重复

    • 本质重复:提取公共逻辑
    • 偶然重复:保持重复,允许独立演化
  4. 评估扩展性:未来是否会频繁新增分支?

    • 会扩展:考虑引入策略模式
    • 不会扩展:保留 if-else,不要过度设计

思考的时候要明白

  1. 隔离比消除更重要

    • if-else 不是敌人,业务耦合才是
    • 将分支隔离到独立的类中,比用策略模式消除 if-else 更有价值
    • 隔离后的 if-else 只是路由逻辑,很清晰,不需要优化
  2. 简单比优雅更重要

    • 3 个分支的 if-else 比策略模式更容易理解
    • 不要为了遵循"开闭原则"而引入不必要的抽象
    • 当业务确实需要扩展时,再引入策略模式也不迟
  3. 风险可控比代码优雅更重要

    • 允许偶然重复,比强行合并导致未来修改相互影响更安全
    • 保留简单的 if-else,比过度抽象导致代码难以理解更好

作为普通的程序员,我们的首要任务是让代码的修改影响面可控,而不是写出"看起来很高级"的代码。在实际工作中:

  • 一个业务隔离清晰的 if-else,胜过一个过度设计的策略模式
  • 一个事务边界明确的编程式事务,胜过一个可能失效的声明式事务
  • 一段重复但独立演化的代码,胜过一个强行合并的公共方法

叠甲叠甲: 我不是反对把if-else重构成策略模式,这完全没问题,是个人的选择。我只是if-else分支太多不是我们重构代码的核心理由,通过消解ifelse来控制风险才是原因。

相关推荐
回家路上绕了弯3 小时前
高并发后台系统设计要点:从流量削峰到低延迟的实战指南
分布式·后端
中微子3 小时前
🚀 2025前端面试必考:手把手教你搞定自定义右键菜单,告别复制失败的尴尬
javascript·面试
Yefimov4 小时前
3. DPDK:更好的压榨cpu--并行计算
后端
jump6804 小时前
js中数组详解
前端·面试
不知道累,只知道类4 小时前
Java 在AWS上使用SDK凭证获取顺序
java·aws
两万五千个小时4 小时前
LangChain 入门教程:06LangGraph工作流编排
人工智能·后端
oak隔壁找我4 小时前
MyBatis的MapperFactoryBean详解
后端
王道长AWS_服务器4 小时前
AWS Elastic Load Balancing(ELB)—— 多站点负载均衡的正确打开方式
后端·程序员·aws
咖啡Beans4 小时前
SpringBoot2.7集成Swagger3.0
java·swagger