文章 548
评论 5
浏览 173080
一个 if-else 写了 800 行,产品让我加一个条件我加了三天

一个 if-else 写了 800 行,产品让我加一个条件我加了三天

新来的同事第一次接手营销规则模块,打开 CouponStrategy.java 沉默了五分钟。

800 行,全是 if-else。

if ("FULL_REDUCTION".equals(couponType)) {
    if (orderAmount >= 100) {
        if ("VIP".equals(userLevel)) {
            if (isFirstOrder) {
                discount = orderAmount * 0.7;
            } else {
                discount = orderAmount * 0.8;
            }
        } else {
            if (isFirstOrder) {
                discount = orderAmount * 0.85;
            } else {
                discount = orderAmount * 0.9;
            }
        }
    } else {
        if ("VIP".equals(userLevel)) {
            discount = 5;
        } else {
            discount = 0;
        }
    }
} else if ("DISCOUNT".equals(couponType)) {
    // 又 200 行
} else if ("GIFT".equals(couponType)) {
    // 又是 200 行
}

产品说:"双十一加一个新的条件——如果用户是 PLUS 会员,满减优惠再减 5 块。"

听起来简单。但这个条件要嵌到三层 if-else 里的每一层。改了 8 处,测了两天,上线后还是漏了一个分支。漏的那个分支刚好是"非 VIP + 首单 + PLUS 会员"——线上 3 小时多发了几万张多减了 5 块的券。

深层嵌套 if-else 不是代码风格问题,是线上事故温床。


一、深层嵌套到底有多不可维护

把这段代码画成决策树,你会发现每一个新增条件都让分支数翻倍:

[券类型]
         /       \
    [满减]       [折扣]
    /    \       /    \
[vip] [非vip]  [vip] [非vip]
 / \    / \     / \    / \
首单 非 首单 非  首单 非 首单 非

3 个条件 = 8 个分支。产品再加一个条件"PLUS 会员"——每个叶子节点再掰成两个——16 个分支。

实际营销规则远不止 3 个条件。券类型、用户等级、订单金额、是否首单、商品分类、活动时间、PLUS 会员、设备类型……8 个条件就是 256 个分支。没有人能维护 256 个 if-else 分支。

更坑的是——改动不可控。 改一行代码不知道影响了多少个场景。回归测试要覆盖 256 种组合。不走运的话,漏掉的组合恰好是双十一当天卖得最火的那一款商品。


二、方案:决策表 + 规则引擎

核心思路:把决策逻辑从代码里剥离出来,变成一张可读、可改、可测试的表。

这就是决策表(Decision Table)——列出所有条件和对应的结果,规则引擎逐条匹配。

一个满减规则的决策表:

券类型订单金额用户等级是否首单PLUS优惠结果
FULL_REDUCTION>=100VIP7折
FULL_REDUCTION>=100VIP7折-5元
FULL_REDUCTION>=100VIP8折
FULL_REDUCTION>=100VIP8折-5元
FULL_REDUCTION>=100非VIP85折
FULL_REDUCTION>=100非VIP85折-5元
FULL_REDUCTION>=100非VIP9折
FULL_REDUCTION>=100非VIP9折-5元
FULL_REDUCTION<100VIP减5元
FULL_REDUCTION<100非VIP不优惠

虽然表里也有 10 行,但它比 if-else 强在哪?一行就是一个独立的规则,改一行不影响其他行,加一行不碰原来任何一行。

产品说"PLUS 会员再减 5 块"——只需要在所有"结果"列里找到对应行,加一个减 5 的逻辑。改的是规则表,不是代码。


三、用 QLExpress 解析决策表

QLExpress 是阿里的规则引擎,天然支持这种场景。决策表翻译成规则脚本:

@Service
public class CouponRuleEngine {
    
    @Autowired private QLExpressRunner runner;
    
    /**
     * 加载决策表规则——每条规则是一行
     */
    private static final String COUPON_RULES = 
        "if(couponType=='FULL_REDUCTION' && orderAmount>=100 && userLevel=='VIP' && isFirstOrder && !isPLUS) {" +
        "    return orderAmount * 0.7;" +
        "} else if(couponType=='FULL_REDUCTION' && orderAmount>=100 && userLevel=='VIP' && isFirstOrder && isPLUS) {" +
        "    return orderAmount * 0.7 - 5;" +
        "} else if(couponType=='FULL_REDUCTION' && orderAmount>=100 && userLevel=='VIP' && !isFirstOrder && !isPLUS) {" +
        "    return orderAmount * 0.8;" +
        "} else if(couponType=='FULL_REDUCTION' && orderAmount>=100 && userLevel=='VIP' && !isFirstOrder && isPLUS) {" +
        "    return orderAmount * 0.8 - 5;" +
        "}" +
        // ... 其他规则
        "else {" +
        "    return 0; // 不优惠" +
        "}";
    
    public BigDecimal executeCoupon(CouponContext ctx) {
        Map<String, Object> params = new HashMap<>();
        params.put("couponType", ctx.getCouponType());
        params.put("orderAmount", ctx.getOrderAmount().doubleValue());
        params.put("userLevel", ctx.getUserLevel());
        params.put("isFirstOrder", ctx.isFirstOrder());
        params.put("isPLUS", ctx.isPLUS());
        
        Object result = runner.execute(COUPON_RULES, params, null);
        return BigDecimal.valueOf(((Number) result).doubleValue());
    }
}

但这样还是手写 if-else 字符串,产品改不了,出错了排查也费劲。


四、更好的方式:DRT 表格存数据库

把决策表存在数据库里,运营在后台页面用表格编辑,规则引擎自动加载解析。

4.1 数据库表设计

CREATE TABLE drt_coupon_rule (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    rule_code VARCHAR(64) NOT NULL,       -- 规则编码
    priority INT DEFAULT 0,                -- 优先级,越小越先匹配
    
    -- 条件列(动态列用 JSON)
    conditions JSON NOT NULL,              -- {"couponType":"FULL_REDUCTION","orderAmount":"[100,)","userLevel":"VIP","isFirstOrder":true}
    
    -- 结果
    result_type VARCHAR(32),               -- DISCOUNT / FIXED / GIFT
    result_value JSON,                     -- {"discount":"0.7","extraDiscount":"5","extraCondition":"isPLUS"}
    
    -- 状态
    status VARCHAR(16) DEFAULT 'ONLINE',
    version INT DEFAULT 0,
    created_at DATETIME DEFAULT NOW(),
    updated_at DATETIME DEFAULT NOW()
);

conditions 是 JSON,存的是"条件名 → 条件值"的映射:

{
    "couponType": "FULL_REDUCTION",
    "orderAmount": "[100,)",
    "userLevel": "VIP",
    "isFirstOrder": true
}

orderAmount 的值 [100,) 是区间表达式——大于等于 100,没有上限。如果要表达"100 到 500 之间",就是 [100,500]

4.2 规则引擎加载 DRT

@Component
public class DrtRuleEngine {
    
    @Autowired private JdbcTemplate jdbc;
    @Autowired private QLExpressRunner runner;
    
    /**
     * 从数据库加载决策表,转成 QLExpress 脚本
     */
    public String buildScript(String ruleCode) {
        List<RuleRow> rows = jdbc.query(
            "SELECT * FROM drt_coupon_rule WHERE rule_code = ? AND status = 'ONLINE' " +
            "ORDER BY priority ASC",
            new BeanPropertyRowMapper<>(RuleRow.class), ruleCode
        );
        
        StringBuilder script = new StringBuilder();
        
        for (RuleRow row : rows) {
            script.append("if(");
            
            // 拼接条件表达式
            String conditionExpr = buildCondition(row.getConditions());
            script.append(conditionExpr);
            script.append(") {");
            
            // 拼接结果
            appendResult(script, row);
            
            script.append("} else ");
        }
        
        script.append("{ return 0; }");  // 默认返回
        
        return script.toString();
    }
    
    /**
     * JSON 条件 → QLExpress 表达式
     */
    private String buildCondition(JSONObject conditions) {
        List<String> parts = new ArrayList<>();
        
        for (String key : conditions.keySet()) {
            Object value = conditions.get(key);
            
            if ("orderAmount".equals(key)) {
                // "[100,)" → "orderAmount >= 100"
                parts.add(parseRange(key, (String) value));
            } else if (value instanceof Boolean) {
                parts.add(key + "==" + value);
            } else if (value instanceof Number) {
                parts.add(key + "==" + value);
            } else {
                parts.add(key + "=='" + value + "'");
            }
        }
        
        return String.join(" && ", parts);
    }
    
    /**
     * 解析区间表达式:"[100,)" → "orderAmount >= 100"
     *                        "[100,500]" → "orderAmount >= 100 && orderAmount <= 500"
     *                        "(100,)" → "orderAmount > 100"
     */
    private String parseRange(String field, String rangeExpr) {
        String inner = rangeExpr.substring(1, rangeExpr.length() - 1);
        String[] parts = inner.split(",");
        
        String lower = parts[0].trim();
        String upper = parts.length > 1 ? parts[1].trim() : "";
        
        List<String> conditions = new ArrayList<>();
        
        if (!lower.isEmpty()) {
            String op = rangeExpr.startsWith("[") ? ">=" : ">";
            conditions.add(field + " " + op + " " + lower);
        }
        if (!upper.isEmpty()) {
            String op = rangeExpr.endsWith("]") ? "<=" : "<";
            conditions.add(field + " " + op + " " + upper);
        }
        
        return String.join(" && ", conditions);
    }
    
    public BigDecimal execute(String ruleCode, Map<String, Object> context) {
        String script = buildScript(ruleCode);
        Object result = runner.execute(script, context, null);
        return BigDecimal.valueOf(((Number) result).doubleValue());
    }
}

4.3 运营后台界面

运营看到的是这样一张表:

┌──────┬──────────┬────────┬──────┬──────┬──────────┐
│ 优先级 │ 券类型     │ 订单金额  │ 用户  │ 首单  │ 优惠结果   │
├──────┼──────────┼────────┼──────┼──────┼──────────┤
│  1   │ 满减      │ ≥100  │ VIP  │ 是   │ 7折      │
│  2   │ 满减      │ ≥100  │ VIP  │ 是   │ 7折-5元  │ +PLUS会员
│  3   │ 满减      │ ≥100  │ VIP  │ 否   │ 8折      │
│  4   │ 满减      │ ≥100  │ 非VIP│ 是   │ 85折     │
│  5   │ 满减      │ ≥100  │ 非VIP│ 否   │ 9折      │
│  6   │ 满减      │ <100  │ —    │ —    │ 减5元    │
│  7   │ 满减      │ <100  │ —    │ —    │ 不优惠   │
└──────┴──────────┴────────┴──────┴──────┴──────────┘
[+ 新增规则] [保存] [发布]

点编辑就是一列一列填,不用写代码。加了"PLUS 会员"这个条件后,原有规则不动,只新增带 PLUS 的行。

优先级决定匹配顺序——先匹配优先级小的行。比如"满减 ≥100 VIP 首单"在优先级 1,"满减 ≥100 VIP"在优先级 10,首单的优先匹配。


五、灰度发布 + 规则测试

决策表改完直接上线等于裸奔。需要加一层规则验证。

@Component
public class RuleValidator {
    
    @Autowired private DrtRuleEngine engine;
    
    /**
     * 批量回归测试——用历史订单数据跑一遍新规则
     */
    @PostMapping("/rule/validate")
    public ValidationResult validate(@RequestBody RulePublishRequest req) {
        
        // 查最近 7 天的订单数据作为测试集
        List<CouponContext> testCases = orderRepo.findRecentOrders(7);
        
        int total = 0, changed = 0;
        List<DiffRecord> diffs = new ArrayList<>();
        
        for (CouponContext ctx : testCases) {
            // 老规则结果
            BigDecimal oldResult = engine.execute("COUPON_V1", ctx.toMap());
            // 新规则结果
            BigDecimal newResult = engine.execute("COUPON_V2", ctx.toMap());
            
            total++;
            if (oldResult.compareTo(newResult) != 0) {
                changed++;
                diffs.add(new DiffRecord(ctx.getOrderId(), oldResult, newResult));
            }
        }
        
        return new ValidationResult(total, changed, diffs);
    }
}

发布前,运营点一下"验证",系统用最近 7 天的真实订单跑一遍新规则,告诉运营有多少订单的优惠金额会变。变了多少、哪些订单变了,一目了然。心里有数才敢点"发布"。


六、效果对比

指标if-else 阶段决策表 + 规则引擎
新增一个条件改 8 处代码,测 2 天加一行规则,2 分钟
回归测试256 个组合手动测历史订单自动跑
代码行数800 行0 行(规则在数据库)
谁在维护只有开发者运营可以自己配
上线风险每次改都怕漏分支验证结果告诉你哪些订单会变
新人上手看 3 天代码看 1 眼决策表就懂

最大的变化不是技术上的——是责任边界变了。以前产品提需求、开发改代码、测试跑回归,一个条件改三天。现在产品提需求、运营在后台配规则、点验证看到影响范围、点发布——十分钟上线。


七、注意事项

注意一:决策表不是银弹。 条件超过 10 个时,决策表的行数会指数级膨胀(2^10 = 1024 行)。这种场景不适合用穷举决策表,需要用规则引擎的 DSL(如 Drools 的 DRL 或者 QLExpress 脚本直接写表达式),允许条件组合而非穷举。

注意二:优先级冲突检测。 两个规则的条件有重叠但优先级不同时,高优先级会覆盖低优先级——这可能是特性也可能是 bug。需要加一个冲突检测:

// 检测:是否有两条规则的条件完全重叠
for (int i = 0; i < rows.size(); i++) {
    for (int j = i + 1; j < rows.size(); j++) {
        if (rows.get(i).covers(rows.get(j))) {
            log.warn("规则 {} 覆盖了规则 {}: 同条件但优先级不同", 
                rows.get(i).getId(), rows.get(j).getId());
        }
    }
}

注意三:区间解析的边界。 [100,) 的右边界是无穷,但实际订单金额不可能无穷。需要让运营感知到——单价不能超过某个值,超出直接拒绝:

if (orderAmount > 1000000) {
    throw new IllegalArgumentException("订单金额超过限制");
}

注意四:决策表版本管理。 运营改了规则、发布了,出问题了要能回滚。给决策表加版本号,每次发布留一个快照。回滚就是切换版本号。


if-else 嵌套是代码里最常见的坏味道。但很多人以为"把它改成 switch-case"或者"提取方法"就能解决问题——不能。嵌套的本质是爆炸的组合数,解决它的唯一方式是把组合从代码里抬出来,变成可管理的数据。

你的项目里有没有那种"谁都不敢动"的深层 if-else?评论区说说最多嵌套了几层。


标题:一个 if-else 写了 800 行,产品让我加一个条件我加了三天
作者:jiangyi
地址:http://jiangyi.space/articles/2026/06/25/1782024763419.html
公众号:服务端技术精选

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程

取消