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) —— 它几乎不做任何实际工作,只是传递调用。
📌 延伸思考:代码异味不是银弹,但它们是识别代码问题的“指南针”。掌握它们,有助于我们在日常开发中写出更清晰、更易维护的代码。