代码可读性实战——写出让同事不骂你的代码,具体怎么做
代码可读性实战——写出让同事不骂你的代码,具体怎么做
适读人群:工作1-5年的开发者,想让自己的代码质量上一个台阶 | 阅读时长:约15分钟 | 核心价值:具体可操作的代码可读性提升技巧,不是原则,是动作
我做 Code Review 大概做了三年,见过各种风格的代码。我发现有一类工程师,写代码很努力,功能都能跑通,但每次看他的 PR 都有一种说不出的难受感——不是逻辑错,是读起来费劲。
这类工程师不是水平差,只是没有在"可读性"这件事上系统思考过。
今天我想很具体地讲,怎么提升代码的可读性。不讲"要写有意义的变量名"这种废话,讲具体的、你明天就能用上的动作。
可读性的本质是什么
在讲具体技巧之前,我想先说一个元认知:代码是写给人看的,顺带让机器执行。
这句话很多人都听过,但理解深度不一样。"给人看"不只是意味着"变量名要起好",而是意味着:读者在读你的代码时,脑子里要维护的"工作内存"越小越好。
人的工作记忆是有限的。如果读者要同时记住10个变量的含义、追踪3个嵌套的状态变化、理解一个5层深的逻辑嵌套,他的认知负担就会超出极限,开始出错,开始崩溃。
所以提升可读性,本质上是在减少读者的认知负担。每一个具体技巧,都服务于这个目标。
命名:最被低估的技能
变量名要说明"是什么",不是"怎么来的"
不好的命名:
int result = userService.getUserCount(departmentId);
List<User> list2 = userService.getActiveUsers(departmentId);好的命名:
int totalUserCount = userService.getUserCount(departmentId);
List<User> activeUsers = userService.getActiveUsers(departmentId);区别在哪里?result 和 list2 说的是"我怎么得到这个值"(某个计算的结果、第二个列表),而 totalUserCount 和 activeUsers 说的是"这个值是什么"。
读者关心的是"这个值是什么",不关心"它是第几个结果"。
布尔变量和方法:用 is/has/can 开头
// 不好
boolean flag = user.checkExpired();
boolean status = order.getPaymentStatus();
// 好
boolean isExpired = user.isExpired();
boolean isPaid = order.isPaid();布尔值有一种特殊的认知负担:你要记住"这个值是 true 表示什么状态"。用 is/has/can 开头的命名,让 true/false 的含义一目了然。
集合类变量用复数
// 不好
List<Order> orderList = ...; // 冗余的 List 后缀
List<User> data = ...; // 完全不知道是什么
// 好
List<Order> orders = ...;
List<User> activeUsers = ...;Java 开发者有一个坏习惯:喜欢给集合变量加 List、Map、Array 后缀。其实类型信息已经在声明里了,变量名里不需要重复。用复数形式,更简洁,也更英语化。
方法名要用动词短语,精确到行为
// 模糊
public void handleUser(User user) {...}
public void processOrder(Order order) {...}
// 精确
public void sendWelcomeEmail(User user) {...}
public void cancelUnpaidOrder(Order order) {...}"handle" 和 "process" 是世界上最没用的动词,因为它们可以表示任何事情。方法名要精确到"做什么",如果做了多件事,考虑拆方法。
函数长度和单一职责
我的实践经验:一个方法超过30行,就要想想能不能拆。不是硬规定,是一个警觉信号。
但更重要的原则是:一个方法只做一件事。
什么叫"一件事"?有一个很好的测试方法:你能不能用一句话(不用"以及""还有""然后"这类连词)来描述这个方法做了什么?
如果你的描述是"先校验订单,然后扣库存,然后发消息,再更新状态",那这个方法就做了四件事,应该拆成四个方法,再加一个协调方法来调用这四个。
一个真实的重构对比
重构前(120行的方法,我节选逻辑结构):
public OrderResult createOrder(OrderRequest request) {
// 校验用户
if (request.getUserId() == null) { throw ... }
User user = userRepository.findById(request.getUserId());
if (!user.isActive()) { throw ... }
// 校验商品
for (OrderItem item : request.getItems()) {
Product product = productRepository.findById(item.getProductId());
if (product == null || product.getStock() < item.getQuantity()) { throw ... }
}
// 计算价格
BigDecimal total = BigDecimal.ZERO;
for (OrderItem item : request.getItems()) {
// 复杂的优惠券、折扣计算...
}
// 扣库存
for (OrderItem item : request.getItems()) {
inventoryRepository.deduct(item.getProductId(), item.getQuantity());
}
// 保存订单
Order order = new Order(...);
orderRepository.save(order);
// 发消息
messagingService.sendOrderCreatedEvent(order);
return OrderResult.success(order.getId());
}重构后:
public OrderResult createOrder(OrderRequest request) {
validateUser(request.getUserId());
validateInventory(request.getItems());
BigDecimal totalAmount = calculateTotalAmount(request.getItems(), request.getCouponCode());
deductInventory(request.getItems());
Order order = buildAndSaveOrder(request, totalAmount);
publishOrderCreatedEvent(order);
return OrderResult.success(order.getId());
}重构后的版本,即使你不看那5个私有方法的实现,光看 createOrder 本身,你也能在10秒内理解"创建订单这个流程大概分哪几步"。这就是可读性的核心价值:让读者能快速建立正确的心理模型。
注释:什么时候写,写什么
这是一个争议很大的话题。有人说"好代码不需要注释",有人说"注释要写得很详细"。我的观点是:
注释要解释"为什么",不要解释"是什么"。
代码本身已经在说"是什么"——变量名、方法名、类型声明都在说明代码在做什么。如果你的代码需要注释来解释"它在做什么",通常意味着代码本身的命名还不够好。
但"为什么"是代码本身无法表达的——为什么这里要加一个 Thread.sleep(100)?为什么这里的超时是3秒而不是5秒?为什么这里要用 synchronized 而不是 ConcurrentHashMap?
这些背后的原因,才是注释应该记录的。
几个例子:
// 不好的注释(解释了是什么,代码已经说清楚了)
// 遍历用户列表
for (User user : users) {
// 好的注释(解释了为什么)
// 微信支付要求金额单位为分,这里乘以100转换,注意精度用整数避免浮点数问题
int amountInFen = amount.multiply(BigDecimal.valueOf(100)).intValue();
// Thread.sleep 是为了规避微博API的限流策略,每秒最多5次请求
Thread.sleep(200);还有一种注释值得写:临时 Hack 的说明。
// TODO: 这里是临时方案,等库存系统升级后要改成推模式,@老张 2024-03-15
List<Product> products = inventoryService.getStockByPolling(skuIds);这类注释要标注时间和负责人,防止 TODO 永远停留在"待办"状态。
错误处理:不要吞掉异常
可读性差的代码里有一个很常见的反模式:
try {
doSomething();
} catch (Exception e) {
// 什么都不做
}或者:
try {
doSomething();
} catch (Exception e) {
e.printStackTrace(); // 只打印了堆栈,没有上下文
}这种写法会让线上 Bug 变得极难排查,因为错误被静默吃掉了。
更好的做法:
try {
doSomething();
} catch (InventoryNotEnoughException e) {
log.warn("下单失败,库存不足. orderId={}, productId={}", orderId, productId, e);
throw new BusinessException(ErrorCode.INVENTORY_NOT_ENOUGH, e);
} catch (Exception e) {
log.error("下单发生未知异常. orderId={}", orderId, e);
throw new SystemException("下单失败", e);
}关键点:
- 捕获具体异常,不要笼统 catch Exception(除非是顶层的兜底处理)
- 日志里要包含上下文信息(orderId、userId 之类的定位字段)
- 不要只 catch 不 throw,要么处理,要么转换成业务异常继续抛出
避免"意外":让代码的行为符合预期
有一种可读性问题叫"最小惊讶原则"(Principle of Least Astonishment):代码的行为应该符合读者的直觉预期。
举个例子:
// 方法名叫 getUser,但它除了返回 user,还会更新用户的最后访问时间
public User getUser(Long userId) {
User user = userRepository.findById(userId);
user.setLastAccessTime(LocalDateTime.now()); // 副作用!
userRepository.save(user);
return user;
}这个方法的名字是 getUser,读者预期它是一个纯查询操作,但实际上它有一个"悄悄更新数据库"的副作用。这种"意外"会导致 Bug 难以定位,也会让读者不敢随便调用这个方法。
解决方法是:要么把名字改成能表达副作用的(比如 getUserAndUpdateAccessTime),要么把副作用分离出去(让调用方显式决定是否要更新)。
一个可以立刻用上的小习惯
每次提交代码之前,我会做一件事:用"陌生人视角"重读一遍自己刚写的代码。
假设我是一个刚加入这个项目、没有任何上下文的工程师,我能在5分钟内理解这段代码的意图吗?
如果不能,找出让我困惑的地方,改掉它。
这个习惯一开始会让你提交代码的速度变慢,但坚持一个月之后,你会发现写代码的时候就自然会往可读性方向写,不再需要事后专门修改。
代码是一种沟通工具。写好代码,也是一种职业素养。
