1. 引言

优秀的程序员都希望写出高质量的软件。但现实是,写出真正“好”的软件远比我们最初设想的要复杂得多。

在本文中,我们将深入探讨软件质量代码异味(Code Smells)之间的关系。代码异味是代码中可能暗示设计或实现问题的特征。识别它们,有助于我们提升代码的可维护性,从而间接提升整个系统的质量。

2. 软件需求

写出好软件并不容易,因为它必须同时满足多个目标:

✅ 提供有用的功能
✅ 尽量无 Bug
✅ 界面友好
✅ 响应速度快
✅ 不占用过多资源
✅ 易于部署、维护和扩展

这些目标之间往往存在冲突,我们需要在它们之间做出权衡。

不过,有些目标比其他目标更重要。Mich Ravera 曾说:

如果它无法正常运行,运行得再快也没用。

可维护性虽然常常排在优先级之后,但它往往是其他质量的“基础”。可维护性强的代码通常 Bug 更少、性能更好、部署也更容易。 所以,如果我们能写出可维护的代码,其他方面自然也会得到提升。

3. 代码质量

代码质量直接影响可维护性,但它本身也是一个复杂的话题。我们通常更容易识别“坏代码”的特征,而不是定义“好代码”的标准。

当你面对一段糟糕的代码时,即使你一时说不清问题出在哪,也会觉得“这代码有点味道”。这种“代码异味”可能表现为:

❌ 难以理解代码的意图
❌ 修改困难
❌ 扩展不易

更糟的是,代码异味往往成群出现。这背后有心理原因:我们来看一下代码库是如何演化的。

3.1 代码库的演化

当面对一个质量低的代码库时,我们通常不会刻意写出高质量的代码来改善它,而是“随大流”地继续添加“垃圾”。我们心里想:“反正已经很烂了,多加一点也没啥。”

一个异味本身影响不大,但如果我们每次都这么干,代码就会越来越“臭”,最终导致整个系统腐化。于是我们开始说:

“别碰这个模块,上次调试它花了一周”
“最好整个重写”

虽然“重写”听起来很诱人,但往往低估了其工作量。更现实的做法是:逐步重构

我们可以识别出部分模块,单独重构而不影响其他部分。完成一个模块后,再继续下一个。这种迭代方式在大多数情况下更可行。

⚠️ 最终决定系统质量的,不是是否重写了代码,而是我们是否改变了开发行为。如果仍然沿用旧习惯,无论是否重写,最终还是会得到另一个烂摊子。

3.2 如何写出好代码

关键在于保持警觉。不要被“烂环境”同化。不要试图修复问题,而是要预防问题。无论周围代码多烂,我们都应该写出干净、高质量的代码。

随着时间推移,老的异味部分会变得越来越刺眼,与新代码格格不入。这时候,我们就能更容易识别它们并进行改进。

幸运的是,我们经常重复犯同样的错误。聪明人已经识别出这些模式,并将它们总结为“代码异味”。

4. 常见代码异味类型

代码异味有多种类型,每种类型代表不同的设计或实现问题。我们下面列出常见的几类,不求穷尽,只求理解其本质。

4.1 膨胀类(Bloaters)

这类异味表现为类或方法过于庞大,难以维护。比如:

❌ 方法参数过多
❌ 类行数过多
❌ 数据重复传递(Data Clumps)

例如下面这个 DateUtil 类:

class DateUtil {
    boolean isAfter(int year1, int month1, int day1, int year2, int month2, int day2) {
        // implementation
    }
  
    int differenceInDays(int year1, int month1, int day1, int year2, int month2, int day2) {
        // implementation
    }
  
    // other date methods
}

所有方法都操作日期数据,参数重复。可以将它们封装为 Date 类:

class Date {
    int year;
    int month;
    int day;
}

class DateUtil {
    boolean isAfter(Date date1, Date date2) {
        // implementation
    }
  
    int differenceInDays(Date date1, Date date2) {
        // implementation
    }
  
    // other date methods
}

更好的做法是把这些方法移到 Date 类中,实现数据与行为的封装。

4.2 面向对象滥用(Object-Orientation Abusers)

这类异味通常是因为没有遵循面向对象设计原则,例如:

❌ 使用大量 switch-case 替代多态
❌ 过度继承
❌ 错误的类职责划分

例如:

class Animal {
    String type;
  
    String makeSound() {
        switch (type) {
            case "cat":
                return "meow";
            case "dog":
                return "woof";
            default:
                throw new IllegalStateException();
        }
    }
}

更好的做法是使用多态:

abstract class Animal {
    abstract String makeSound();
}

class Cat extends Animal {
    @Override
    String makeSound() {
        return "meow";
    }
}

class Dog extends Animal {
    @Override
    String makeSound() {
        return "woof";
    }
}

这样不仅去除了 switch-case,还避免了非法状态。

4.3 阻碍变更(Change Preventers)

这类异味违反了单一职责原则,导致:

❌ 修改一个功能需要改动多个地方(Shotgun Surgery)
❌ 多个功能修改都需要改动同一个地方(Divergent Change)

这类问题通常出现在模块设计不合理时。但要注意,并非所有类似结构都是异味。例如,抽象工厂模式中会故意使用“平行继承结构”,这是设计上的选择,而不是错误。

4.4 可有可无(Dispensables)

这类异味通常表现为代码中的“噪音”,例如:

❌ 无意义的注释
❌ 多余的变量
❌ 没有实际作用的代码

例如下面这段代码:

// amount
double a = order.getAmount();
// discount factor
double b = 1;
if (a > 10) {
    b = 0.9;
}
// discounted price
double c = product.getPrice() * b;
// order sum price
double d = a * c;

使用有意义的变量名可以去掉注释:

double amount = order.getAmount();
double discountFactor = 1;
if (amount > 10) {
    discountFactor = 0.9;
}
double discountedPrice = product.getPrice() * discountFactor;
double orderSumPrice = amount * discountedPrice;

⚠️ 注意:不是所有注释都是坏的。如果注释解释了“为什么”要这么做(比如某个特殊业务规则),它就是有价值的。

4.5 耦合器(Couplers)

这类异味导致类之间耦合严重,难以独立修改。例如:

❌ 不恰当的亲密关系(访问私有成员)
❌ 方法链(Message Chains)
❌ 中间人(Middle Man)

例如:

class Repository {
    Entity findById(long id) {
        // implementation
    }
}

class Service {
    Repository repository;

    Repository getRepository() {
        return repository;
    }
}

class Context {
    Service service;

    void useCase() {
        Entity entity = service.getRepository().findById(1);
        // using entity
    }
}

可以改为:

class Service {
    Repository repository;

    Entity findById(long id) {
        return repository.findById(id);
    }
}

class Context {
    Service service;

    void useCase() {
        Entity entity = service.findById(1);
        // using entity
    }
}

但此时 Service.findById() 又成了“中间人”异味。因此,是否重构取决于具体场景。

4.6 其他问题

⚠️ 注意:识别和修复代码异味不是万能药。有些结构在特定上下文中是合理的,例如抽象工厂模式中的“平行继承结构”。

另外,有时修复一个异味会导致另一个异味出现,比如中间人 vs 方法链。这时候,理解代码意图是做出正确决策的关键。

5. 如何消除代码异味?

我们前面提到,预防代码异味的关键是写出干净的代码,不被烂代码带偏。

但有时异味已经存在。这时,我们可以通过识别代码异味来定位问题代码。要解决这些问题,通常使用重构(Refactoring)技术:

✅ 不改变外部行为的前提下,改善代码结构
✅ 持续重构可以逐步提升代码质量
✅ 它是应对代码异味的首选手段

我们不会深入重构技巧,但可以参考《重构》一书或我们之前的文章。

6. 总结

代码异味是代码质量下降的早期信号。识别它们有助于我们主动改善代码结构,从而提升系统的可维护性、稳定性和扩展性。

在本文中,我们讨论了:

✅ 代码质量与可维护性的关系
✅ 常见的代码异味类型及其示例
✅ 如何通过重构消除异味

最后,前面提到的 Service 类在示例中其实是一个懒惰类(Lazy Class) —— 它几乎不做任何实际工作,只是传递调用。


📌 延伸思考:代码异味不是银弹,但它们是识别代码问题的“指南针”。掌握它们,有助于我们在日常开发中写出更清晰、更易维护的代码。


原始标题:Code Smells