代码坏味道(下):霰弹式修改、依恋情结、数据泥团、基本类型偏执、switch语句滥用
好,咱们接着聊代码坏味道。上一章讲了几个常见的,这一章我挑了几个更隐蔽、更让人头疼的。说实话,这些坏味道在项目里出现的频率极高,而且往往不是一眼就能看出来的。
我个人习惯,每次重构前都会先扫一遍代码,看看有没有下面这五种味道。一旦发现,基本就是重构的信号。
霰弹式修改:改一处,炸一片
什么叫霰弹式修改?你想想看,改一个需求,你得跑遍十几个文件,每个文件改那么一两行。就像打霰弹枪,一打一大片,但每一颗都打不中要害。
我在项目中遇到过最典型的例子:一个电商系统的订单状态变更。需求很简单,加一个“已取消”状态。结果呢?我改了订单服务、支付服务、物流服务、用户通知服务……整整改了8个文件。改完之后,我后背都湿了——生怕漏了哪个。
为什么会这样?说白了,就是没有把变化点封装好。本该在一个地方做的事,散落得到处都是。
重构手法:搬移函数 + 内联类。把散落的逻辑收拢到一个地方。比如订单状态变更,就应该统一收拢到订单聚合根里。
依恋情结:一个类总想跟别的类“谈恋爱”
这个坏味道很有意思。一个方法,明明属于A类,却对B类的数据特别感兴趣。它的大部分操作都在访问B类的数据,而不是自己类的数据。
我记得有一次review代码,看到一个OrderService里的方法,80%的代码都在操作User对象的数据。我当时就问:这个方法是不是应该放在UserService里?
// 坏味道:OrderService 对 User 的数据过于依恋
public class OrderService {
public boolean canPlaceOrder(Order order, User user) {
// 这里大量使用 user 的数据
if (user.getVipLevel() > 3 && user.getCreditScore() > 700) {
return true;
}
// 反而 order 的数据用得很少
return order.getTotalAmount() < 10000;
}
}
重构手法:把方法搬移到它真正依恋的类里。上面的例子,canPlaceOrder 就应该搬到 User 类里。
数据泥团:总是一起出现的数据
你写代码的时候,有没有发现某些数据总是成对出现?比如地址里的省、市、区、街道,或者订单里的商品ID、数量、单价。
这些数据就像泥巴一样,黏在一起。你拆不开,也不应该拆开。但很多新手喜欢把它们拆成一个个基本类型参数,结果就是方法签名越来越长。
// 坏味道:数据泥团
public void createOrder(String productId, int quantity, double price,
String province, String city, String district, String street) {
// ...
}
// 重构后:提取为值对象
public void createOrder(ProductItem item, Address address) {
// ...
}
我个人习惯,一旦发现三个以上参数总是同时出现,就会考虑提取成一个类。这不仅仅是代码整洁的问题,更是为了后续扩展。你想想看,如果以后地址要加一个“邮编”,你只需要改Address类,而不是改所有用到地址的方法。
基本类型偏执:什么都是String和int
这个坏味道跟数据泥团有点像,但更底层。它指的是:过度使用基本类型(String、int、long等),而不愿意创建有意义的类型。
比如,用String表示电话号码,用int表示年龄,用String表示邮箱。看起来没问题,对吧?但问题在于:基本类型没有业务含义,也没有约束。
// 坏味道:基本类型偏执
public void registerUser(String name, String phone, String email, int age) {
// 这里 phone 可以是任意字符串,甚至可以是 "abc"
// age 可以是负数
}
// 重构后:使用值对象
public class PhoneNumber {
private final String value;
public PhoneNumber(String value) {
if (value == null || !value.matches("\\d{11}")) {
throw new IllegalArgumentException("无效的手机号");
}
this.value = value;
}
}
public void registerUser(String name, PhoneNumber phone, Email email, Age age) {
// 现在参数有了业务含义和约束
}
我记得有一次,一个同事用String存了用户ID,结果不小心传了一个带空格的字符串进去,查了半天才发现。如果用了UserId类型,这种问题根本不会发生。
switch语句滥用:到处是if-else和switch
这个坏味道,说白了就是面向过程编程的遗毒。你看到switch语句,就要想到多态。switch语句本身不是问题,问题在于:它往往意味着代码违反了开闭原则。
为什么?因为每增加一种新情况,你就要修改这个switch语句。而多态的做法是:新增一个子类,不需要修改现有代码。
// 坏味道:switch 语句滥用
public double calculateShippingCost(String shippingType, double weight) {
switch (shippingType) {
case "standard":
return weight * 1.5;
case "express":
return weight * 3.0;
case "overnight":
return weight * 5.0;
default:
throw new IllegalArgumentException("未知的配送类型");
}
}
// 重构后:使用多态
public interface ShippingStrategy {
double calculate(double weight);
}
public class StandardShipping implements ShippingStrategy {
public double calculate(double weight) {
return weight * 1.5;
}
}
public class ExpressShipping implements ShippingStrategy {
public double calculate(double weight) {
return weight * 3.0;
}
}
我在项目中遇到过最夸张的switch语句,有30多个case分支。每次加一个新类型,都要在这个巨大的switch里加一行。后来我把它重构成了策略模式,代码量减少了60%,而且扩展起来特别方便。
知识体系总览
下面这张图,我把这五种坏味道的关系和重构方向整理了一下。你可以把它当作一个快速参考。
这五种坏味道,说白了都是代码设计上的“偷懒”。要么是懒得封装,要么是懒得抽象,要么是懒得思考。但正是这些“偷懒”,让代码变得越来越难以维护。
我个人习惯,每次提交代码前,都会问自己三个问题:
- 这个改动需要改多个地方吗?——警惕霰弹式修改
- 这个方法是不是太“关心”别的类了?——警惕依恋情结
- 这些参数是不是总一起出现?——警惕数据泥团和基本类型偏执
嗯,今天就聊到这里。代码坏味道这个话题,其实还有很多可以说的。但记住一点:坏味道不是罪,无视坏味道才是。下次你review代码的时候,不妨用这五个标准去扫一遍,你会发现很多可以优化的地方。