代码坏味道(下):霰弹式修改、依恋情结、数据泥团、基本类型偏执、switch语句滥用

好,咱们接着聊代码坏味道。上一章讲了几个常见的,这一章我挑了几个更隐蔽、更让人头疼的。说实话,这些坏味道在项目里出现的频率极高,而且往往不是一眼就能看出来的。

我个人习惯,每次重构前都会先扫一遍代码,看看有没有下面这五种味道。一旦发现,基本就是重构的信号。

霰弹式修改:改一处,炸一片

什么叫霰弹式修改?你想想看,改一个需求,你得跑遍十几个文件,每个文件改那么一两行。就像打霰弹枪,一打一大片,但每一颗都打不中要害。

我在项目中遇到过最典型的例子:一个电商系统的订单状态变更。需求很简单,加一个“已取消”状态。结果呢?我改了订单服务、支付服务、物流服务、用户通知服务……整整改了8个文件。改完之后,我后背都湿了——生怕漏了哪个。

核心问题:一个逻辑变化,分散在多个类或模块中。

为什么会这样?说白了,就是没有把变化点封装好。本该在一个地方做的事,散落得到处都是。

重构手法:搬移函数 + 内联类。把散落的逻辑收拢到一个地方。比如订单状态变更,就应该统一收拢到订单聚合根里。

我的经验:如果你发现改一个需求需要打开超过3个文件,就要警惕了。这往往是霰弹式修改的前兆。

依恋情结:一个类总想跟别的类“谈恋爱”

这个坏味道很有意思。一个方法,明明属于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类,而不是改所有用到地址的方法。

避坑指南:我曾经在一个项目里,看到有人把地址拆成了6个String参数。后来需求变更,要加一个“详细地址”字段,结果改了40多个方法。从那以后,我再也不敢把数据泥团拆开了。

基本类型偏执:什么都是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%,而且扩展起来特别方便。

注意:不是所有switch都要消灭。如果分支很少(比如2-3个),而且不太可能变化,用switch也没问题。但一旦分支超过5个,或者经常变化,就要考虑多态了。

知识体系总览

下面这张图,我把这五种坏味道的关系和重构方向整理了一下。你可以把它当作一个快速参考。

代码坏味道(下)知识体系 五种坏味道 霰弹式修改 依恋情结 数据泥团 基本类型偏执 switch语句滥用 重构手法 搬移函数 + 内联类 提取值对象 多态取代条件表达式 核心原则:识别坏味道 → 选择重构手法 → 保持代码健康 记住:坏味道不是bug,但它们是bug的温床

这五种坏味道,说白了都是代码设计上的“偷懒”。要么是懒得封装,要么是懒得抽象,要么是懒得思考。但正是这些“偷懒”,让代码变得越来越难以维护。

我个人习惯,每次提交代码前,都会问自己三个问题:

  1. 这个改动需要改多个地方吗?——警惕霰弹式修改
  2. 这个方法是不是太“关心”别的类了?——警惕依恋情结
  3. 这些参数是不是总一起出现?——警惕数据泥团和基本类型偏执

嗯,今天就聊到这里。代码坏味道这个话题,其实还有很多可以说的。但记住一点:坏味道不是罪,无视坏味道才是。下次你review代码的时候,不妨用这五个标准去扫一遍,你会发现很多可以优化的地方。