程序员避坑指南

标题灵感来源于 《阿里云崩了,我们更愿意读“事件说明”还是“避坑指南”?》这篇文章

我遇到过最大的坑:一个负号差点让公司损失 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 倍。

编程插件

AI简述
Copilot收费、编程插件、功能强大
通义灵码免费、编程插件
Bito部分功能免费

浏览器中使用

AI简述
ChatGPT 3.5免费
Gemini免费
coze免费、可优化 Prompt、 自定义机器人、插件、工作流、可选:ChatGPT 4
文心一言免费
智谱清言免费
kimi.ai免费
紫东太初免费
Dify部分功能收费
天工免费 APP
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus