标题灵感来源于
《阿里云崩了,我们更愿意读“事件说明”还是“避坑指南”?》这篇文章
我遇到过最大的坑:一个负号差点让公司损失 30 多万
故事是这样的:曾经公司有一款语聊 App,聊天室内用户赠送给主播礼物,主播收到礼物后可以获得报酬,主播报酬 = 礼物价值 ✖️ 主播分成比例。
本次需求是根据主播评级动态来获取的主播分成比例,正常逻辑是先从 Redis 获取。如果 Redis 中没有,就再从数据库获取;如果数据库中也没有,就返回一个默认值,并保存至 Redis。
问题就出在默认值上。正常情况下,主播分成比例应该小于 100%,但由于我的失误,返回了一个无意义的值 -1,在计算主播收益时,所有参与运算的数据都取绝对值后再运算,这样 -1 就变成了 1,主播分成比例也就变成了 100%。
当财务部门给主播结算报酬时发现要多出几十万,这才发现不对劲儿。往日不起眼的小 Bug,瞬间升级成了重大 Bug,幸亏发现及时才没有造成更大损失。
点击查看完整故事
技巧一:使用单一原则,让问题变的更简单
以更新订单状态为例,当混在一起时,代码是这样的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| public void 更新订单状态(订单ID,订单状态){
// 验证订单是否存在
// 验证订单状态是否合法
if(订单状态 == 1){
// 验证是否可以更新为待支付状态
// 待支付逻辑:
} else if(订单状态 == 2){
// 验证是否可以更新为支付中状态
// 支付中逻辑
} else if(订单状态 == 3){
// 支付成功逻辑
} else if(订单状态 == 4){
// 支付成功逻辑:发送 IM 消息通知用户;增加用户消费等级
} else if(订单状态 == 5){
// 支付失败逻辑
} else if(订单状态 == 6){
// 申请退款中逻辑
} else if(订单状态 == 7){
// 退款成功逻辑
// 给用户退款:判断是否已经退款过,没有退过才能退款;保存退款记录;减少用户消费等级
} else if(订单状态 == 3){
// 退款失败逻辑:解析退款失败原因;发送 IM 消息通知用户退款失败原因
}
}
|
剥离与更新状态无关的功能:验证订单是否存在、验证是否可以更新为待支付状态、发送 IM 消息等逻辑,代码是这样的
1
2
3
| public void 更新订单状态(订单ID,订单状态){
// 更新订单状态:根据订单ID,更新数据库中的订单状态
}
|
退款成功逻辑是这样的,如果有良好的命名,就能做到代码即文档
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public void 订单退款已成功(订单ID){
验证订单是否存在(订单ID);
验证订单是否可以更改为新状态(订单ID,新状态为退款成功);
更新订单状态(订单ID,新状态为退款成功)
订单可以退款(订单ID,新状态为退款成功);
保存退款记录(订单ID,新状态为退款成功);
减少用户消费等级(订单ID,新状态为退款成功);
}
public void 验证订单是否存在(订单ID){
// 不存在时抛出异常,终止当前操作
}
public void 验证订单是否可以更改为新状态(订单ID,新状态){
// 不能更新时抛出异常,终止当前操作
}
public void 订单可以退款(订单ID){
// 订单已退款时抛出异常,终止当前操作
}
public void 保存退款记录(订单ID){
// 保存退款记录失败时抛出异常,终止当前操作
}
public void 减少用户消费等级(订单ID){
// 减少用户消费等级失败时抛出异常,终止当前操作
}
|
技巧二:DRY 原则,不要重复自己
当遇到逻辑重复代码时,可以提取出来,形成一个公共方法,避免相同的逻辑散落到各个地方,当出现 Bug 时时,只需要修改一个地方即可,而不用搜遍整个项目。
技巧三:使用单元测试保证核心代码功能复合预期
涉及重要业务场景「财务、业务主流程等」的代码使用单元测试,首先可以确保代码功能复合设计预期,其次可以及时发现 Bug,避免通过 Debug、搜索日志等方式找 Bug,节省大量排错时间。
测试可以分为三个路径
- happy path:正常功能
- sad path:异常判断
- default path:默认值
技巧四:即时重构,让代码越来越好
当有了单元测试做保障时,发现坏味道的代码时,可以放心的重构,使腐化的代码变整洁,使复杂的代码变简单。
简单的代码更容易维护,Bug 更少。
当这四点都做到了,也就是在使用 TDD「测试驱动开发」了!
测试代码写着写着,就实现了测试即文档,例如:
我的 TDD 学习笔记
技巧五:支付业务使用凭证,保证支付安全
寄信时我们会购买一张邮票,将邮票贴到信封上,信件寄出前邮局会核验邮票的价格是否符合投递收费标准,核验通过后会盖上邮戳,表示邮票已使用,避免再次使用。
1
2
3
4
5
6
7
| mermaid 源码
graph LR
A[购买邮票] --> B[核验邮票]
B --> C[核验通过]
C --> D[盖邮戳]
D --> E[邮寄]
|
转换为支付业务中可使用的模型
1
2
3
4
5
6
7
| mermaid 源码
graph LR
A[生成支付凭证] --> B[核验支付凭证]
B --> C[核验通过]
C --> D[标记凭证已使用]
D --> E[使用凭证兑换服务]
|
避免用户薅羊毛的解决方案简化成了:确保一个支付凭证只能消费一次
关于凭证的使用,
来源于这篇文章
技巧六:使用 Map 消除 if-else
当有很多 if-else 时,可以使用 Map 来消除 if-else,使代码更简洁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| String statusName(int statusCode){
if(statusCode == 1){
return "待接单";
} else if(statusCode == 2){
return "已取消";
} else if(statusCode == 3){
return "已拒绝";
} else if(statusCode == 4){
return "对方已取消";
} else if(statusCode == 5){
return "进行中";
} else if(statusCode == 6){
return "已结束";
}
}
|
使用 Map 重构后的代码
1
2
3
4
5
6
7
8
9
10
11
| String statusName(int statusCode){
Map<Integer, String> statusMap = new HashMap<>();
statusMap.put(1, "待接单");
statusMap.put(2, "已取消");
statusMap.put(3, "已拒绝");
statusMap.put(4, "对方已取消");
statusMap.put(5, "进行中");
statusMap.put(6, "已结束");
return statusMap.get(statusCode);
}
|
技巧七:使用 AI 开挂
AI 虽强,但要发挥威力还需自身具备一定的知识管理能力,推荐资源
- 《管理(原书修订版)(上册)》作者彼得·德鲁克:可提升逻辑能力,使 AI 更容易理解我们提出的问题,加速解决时间
- 极客时间专栏
徐昊 · AI 时代的软件工程学习:
传奇程序员 Kent Beck 所言:LLM 让我 90% 的技能无用了,却让 10% 的技能放大了 1000 倍。
编程插件
浏览器中使用