一个 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 | >=100 | VIP | 是 | 否 | 7折 |
| FULL_REDUCTION | >=100 | VIP | 是 | 是 | 7折-5元 |
| FULL_REDUCTION | >=100 | VIP | 否 | 否 | 8折 |
| FULL_REDUCTION | >=100 | VIP | 否 | 是 | 8折-5元 |
| FULL_REDUCTION | >=100 | 非VIP | 是 | 否 | 85折 |
| FULL_REDUCTION | >=100 | 非VIP | 是 | 是 | 85折-5元 |
| FULL_REDUCTION | >=100 | 非VIP | 否 | 否 | 9折 |
| FULL_REDUCTION | >=100 | 非VIP | 否 | 是 | 9折-5元 |
| FULL_REDUCTION | <100 | VIP | — | — | 减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://www.jiangyi.space/articles/2026/06/25/1782024763419.html
公众号:服务端技术精选
评论