第3章:代码的坏味道(Bad Smells in Code)
一、导读
1.1 本章学习目标
第3章是《重构》中最具实践价值的章节之一。Martin Fowler 在本章中系统地列举了各种"代码的坏味道"——那些暗示代码可能需要重构的信号。学会识别这些坏味道,是每位程序员提升代码审查能力的关键技能。
本章的核心学习目标包括:
- 掌握常见代码坏味道的识别方法:学会从代码表面发现深层问题
- 理解每种坏味道背后的设计问题:不只是知道"是什么",更要知道"为什么"
- 建立坏味道与重构手法的映射关系:看到坏味道就知道该用什么手法
- 培养代码审查的直觉:通过练习提高发现问题的能力
- 区分坏味道与合理的代码风格差异:不是所有不寻常的代码都是坏味道
1.2 为什么本章如此重要
Kent Beck 说过:"代码应该像婴儿的屁股——又软又嫩,而且闻起来很香。"遗憾的是,大多数代码并非如此。坏味道是代码在告诉你:"我需要改变。"学会听懂代码的"语言",是区分初级程序员和高级程序员的关键能力。
本章之所以重要,是因为它提供了一套可操作的诊断工具。你不需要等到代码完全不可维护才去重构——当你发现某个坏味道时,就是重构的信号。这种"嗅觉"需要通过实践来培养,而本章就是培养这种嗅觉的起点。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
什么是"代码的坏味道"?它与 bug 有什么区别?
最常见的坏味道有哪些?它们各自暗示了什么设计问题?
如何判断一段代码是"坏味道"还是"合理的代码风格"?
坏味道之间是否有关联?一个坏味道是否会导致其他坏味道?
如何在团队中建立识别和消除坏味道的文化?
重构手法与坏味道之间是什么关系?
二、核心概念详解
2.1 神秘命名(Mysterious Name)
定义:函数、变量或类的名称没有清晰地表达其用途或含义。
为什么是坏味道:名称是代码的文档。好的名称让读者不需要阅读实现就能理解代码的意图。差的名称迫使读者花时间阅读代码来猜测其含义,极大地增加了理解成本。
识别方法:
- 名称过于笼统:
data、info、temp、handle - 名称与功能不匹配:
getData()实际上在做计算 - 名称包含实现细节:
calculateUsingBubbleSort() - 缩写和拼写错误:
usrCnt、calclate
// 坏味道:神秘命名
function calc(x, y) {
return x * y * 0.08;
}
function process(data) {
let t = 0;
for (let i = 0; i < data.length; i++) {
t += data[i].v;
}
return t;
}
// 重构后:清晰的命名
function calculateSalesTax(price, quantity) {
const taxRate = 0.08;
return price * quantity * taxRate;
}
function calculateTotalRevenue(transactions) {
let totalRevenue = 0;
for (const transaction of transactions) {
totalRevenue += transaction.value;
}
return totalRevenue;
}对应的重构手法:Change Function Declaration(改变函数声明)、Rename Variable(重命名变量)、Rename Field(重命名字段)
2.2 重复代码(Duplicated Code)
定义:相同的代码结构出现在多个地方。
为什么是坏味道:重复代码是维护的噩梦。当你需要修改逻辑时,必须找到并修改所有重复的地方。遗漏任何一处都会导致不一致的 bug。正如 Andy Hunt 和 Dave Thomas 在《程序员修炼之道》中所说:"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."(DRY 原则)
识别方法:
- 完全相同的代码块出现在不同函数中
- 结构相似但细节不同的代码
- 复制粘贴的代码,只有变量名不同
// 坏味道:重复代码
function renderUserCard(user) {
let html = '<div class="card">';
html += `<h2>${user.name}</h2>`;
html += `<p>${user.email}</p>`;
html += `<span class="badge">${user.role}</span>`;
html += '</div>';
return html;
}
function renderAdminCard(admin) {
let html = '<div class="card">';
html += `<h2>${admin.name}</h2>`;
html += `<p>${admin.email}</p>`;
html += `<span class="badge">${admin.role}</span>`;
html += `<p>Department: ${admin.department}</p>`;
html += '</div>';
return html;
}
// 重构后:提取公共逻辑
function renderCard(person, extraContent = '') {
return `
<div class="card">
<h2>${person.name}</h2>
<p>${person.email}</p>
<span class="badge">${person.role}</span>
${extraContent}
</div>
`;
}
function renderUserCard(user) {
return renderCard(user);
}
function renderAdminCard(admin) {
return renderCard(admin, `<p>Department: ${admin.department}</p>`);
}对应的重构手法:Extract Function(提炼函数)、Slide Statements(搬移语句)、Pull Up Method(上移方法)
2.3 过长函数(Long Function)
定义:函数体过长,做了太多事情。
为什么是坏味道:过长的函数难以理解、难以测试、难以修改。函数的长度与其认知复杂度成正比——当函数超过 20-30 行时,大多数程序员就很难在脑中保持完整的上下文。
识别方法:
- 函数超过 30 行(经验值,不是绝对标准)
- 函数内有多个注释来分隔不同的逻辑段
- 函数的缩进层次超过 3 层
- 函数名需要用"和"来描述(如"calculateAndValidateAndSave")
// 坏味道:过长函数
function processOrder(order) {
// 验证订单
if (!order.customer) throw new Error("No customer");
if (!order.items || order.items.length === 0) throw new Error("No items");
for (const item of order.items) {
if (item.quantity <= 0) throw new Error("Invalid quantity");
if (item.price < 0) throw new Error("Invalid price");
}
// 计算价格
let subtotal = 0;
for (const item of order.items) {
subtotal += item.price * item.quantity;
}
const tax = subtotal * 0.08;
const shipping = subtotal > 100 ? 0 : 10;
const total = subtotal + tax + shipping;
// 检查库存
for (const item of order.items) {
const stock = inventoryService.getStock(item.productId);
if (stock < item.quantity) {
throw new Error(`Insufficient stock for ${item.name}`);
}
}
// 扣减库存
for (const item of order.items) {
inventoryService.decreaseStock(item.productId, item.quantity);
}
// 处理支付
const payment = paymentService.charge(order.customer.paymentMethod, total);
if (!payment.success) {
throw new Error("Payment failed");
}
// 创建订单记录
const orderRecord = {
id: generateOrderId(),
customer: order.customer.id,
items: order.items,
total: total,
status: 'CONFIRMED',
createdAt: new Date()
};
database.save('orders', orderRecord);
// 发送通知
emailService.sendOrderConfirmation(order.customer.email, orderRecord);
return orderRecord;
}
// 重构后:短小而清晰的函数
function processOrder(order) {
validateOrder(order);
const pricing = calculatePricing(order.items);
checkAndReserveInventory(order.items);
const payment = chargePayment(order.customer, pricing.total);
const orderRecord = createOrderRecord(order, pricing, payment);
sendConfirmation(order.customer, orderRecord);
return orderRecord;
}
function validateOrder(order) {
if (!order.customer) throw new Error("No customer");
if (!order.items?.length) throw new Error("No items");
order.items.forEach(validateItem);
}
function validateItem(item) {
if (item.quantity <= 0) throw new Error("Invalid quantity");
if (item.price < 0) throw new Error("Invalid price");
}
function calculatePricing(items) {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * TAX_RATE;
const shipping = subtotal > FREE_SHIPPING_THRESHOLD ? 0 : SHIPPING_COST;
return { subtotal, tax, shipping, total: subtotal + tax + shipping };
}对应的重构手法:Extract Function(提炼函数)、Replace Temp with Query(以查询取代临时变量)、Introduce Parameter Object(引入参数对象)
2.4 过长参数列(Long Parameter List)
定义:函数的参数过多,通常超过 3-4 个。
为什么是坏味道:过长的参数列难以理解、容易传错参数、且暗示函数可能做了太多事情。参数越多,函数的可能调用组合就呈指数增长,测试的复杂度也随之增加。
识别方法:
- 参数超过 4 个
- 参数中有多个是相关的(可以组合成一个对象)
- 参数中包含标志位(boolean flag)
- 不同调用场景使用不同的参数子集
// 坏味道:过长参数列
function createUser(firstName, lastName, email, phone,
street, city, state, zip, country,
role, department, startDate) {
// ...
}
// 重构后:引入参数对象
function createUser(name, contact, address, employment) {
// ...
}
// 更具体的实现
class Name {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
class Contact {
constructor(email, phone) {
this.email = email;
this.phone = phone;
}
}
class Address {
constructor(street, city, state, zip, country) {
this.street = street;
this.city = city;
this.state = state;
this.zip = zip;
this.country = country;
}
}
function createUser(name, contact, address, employment) {
return {
name: name.fullName,
email: contact.email,
address: address,
role: employment.role,
department: employment.department,
startDate: employment.startDate
};
}对应的重构手法:Introduce Parameter Object(引入参数对象)、Remove Flag Argument(移除标记参数)、Preserve Whole Object(保持完整对象)
2.5 发散式变化(Divergent Change)
定义:一个类或模块因为多种不同的原因而被修改。
为什么是坏味道:发散式变化违反单一职责原则。当一个类需要因为不同的原因被修改时,修改之间可能会相互干扰。更糟糕的是,相关的逻辑散落在多个类中,使得理解和修改都变得困难。
识别方法:
- 一个类频繁因为不同的原因被修改
- 同一个类中的代码服务于不同的业务场景
- 修改一个功能时需要同时修改类中多个不相关的方法
// 坏味道:发散式变化
class Employee {
// 以下方法因为薪酬计算规则变化而修改
calculateSalary() { /* ... */ }
calculateBonus() { /* ... */ }
calculateOvertime() { /* ... */ }
// 以下方法因为报告格式变化而修改
generatePayStub() { /* ... */ }
generateTaxForm() { /* ... */ }
// 以下方法因为权限规则变化而修改
canAccessReport() { /* ... */ }
canModifySalary() { /* ... */ }
}
// 重构后:每个类只有一个变化的原因
class SalaryCalculator {
calculateSalary(employee) { /* ... */ }
calculateBonus(employee) { /* ... */ }
calculateOvertime(employee) { /* ... */ }
}
class ReportGenerator {
generatePayStub(employee) { /* ... */ }
generateTaxForm(employee) { /* ... */ }
}
class PermissionChecker {
canAccessReport(user) { /* ... */ }
canModifySalary(user) { /* ... */ }
}对应的重构手法:Extract Class(提炼类)、Split Phase(分离阶段)、Move Function(搬移函数)
2.6 霰弹式修改(Shotgun Surgery)
定义:一个变化需要修改多个不同的类。
为什么是坏味道:与发散式变化相反,霰弹式修改是指一个变更需要跨越多个文件进行修改。这使得修改变得困难且容易遗漏——你必须记住所有需要修改的地方。
识别方法:
- 每次添加新功能都需要修改很多文件
- 修改一个业务规则需要同时改动多个类
- 代码变更的"影响范围"总是超出预期
// 坏味道:霰弹式修改
// 添加新的订单状态需要修改以下所有文件:
// Order.js - 添加状态常量
// OrderValidator.js - 添加状态转换规则
// OrderService.js - 添加状态处理逻辑
// OrderRepository.js - 添加状态查询
// OrderController.js - 添加状态 API
// OrderNotification.js - 添加状态通知
// 重构后:将相关逻辑集中在一起
class OrderStateMachine {
constructor() {
this.transitions = {
'DRAFT': ['SUBMITTED', 'CANCELLED'],
'SUBMITTED': ['CONFIRMED', 'CANCELLED'],
'CONFIRMED': ['SHIPPED'],
'SHIPPED': ['DELIVERED'],
'DELIVERED': [],
'CANCELLED': []
};
}
canTransition(from, to) {
return this.transitions[from]?.includes(to) || false;
}
transition(order, newState) {
if (!this.canTransition(order.status, newState)) {
throw new Error(`Invalid transition: ${order.status} -> ${newState}`);
}
order.status = newState;
return order;
}
}对应的重构手法:Move Function(搬移函数)、Move Field(搬移字段)、Inline Class(内联类)
2.7 其他常见坏味道
2.7.1 过大的类(Large Class)
一个类试图做太多事情,拥有过多的字段和方法。识别方法:类文件超过 500 行,或者需要滚动才能看完所有方法。
2.7.2 依恋情结(Feature Envy)
一个函数对另一个类的兴趣超过了对自己所在类的兴趣。它频繁访问其他类的数据,而不是调用其行为。
// 坏味道:依恋情结
class Report {
generateSummary(transactions) {
let total = 0;
for (const t of transactions) {
total += t.amount; // 频繁访问 Transaction 的数据
}
return `Total: ${total}`;
}
}
// 重构后:将逻辑移到数据所在的类
class Transaction {
getTotal(transactions) {
return transactions.reduce((sum, t) => sum + t.amount, 0);
}
}
class Report {
generateSummary(transactions) {
const total = new Transaction().getTotal(transactions);
return `Total: ${total}`;
}
}2.7.3 数据泥团(Data Clumps)
多个数据项总是同时出现,比如三个参数总是成对传递。这暗示它们应该被封装成一个对象。
2.7.4 基本类型偏执(Primitive Obsession)
使用基本类型(字符串、数字)来表示领域概念,而不是创建专门的类。比如用字符串表示电话号码、用数字表示金额。
// 坏味道:基本类型偏执
const phoneNumber = "123-456-7890";
const money = 100.50;
// 重构后:用领域对象表达
class PhoneNumber {
constructor(value) {
if (!this.isValid(value)) throw new Error("Invalid phone number");
this.value = value;
}
isValid(value) {
return /^\d{3}-\d{3}-\d{4}$/.test(value);
}
toString() { return this.value; }
}
class Money {
constructor(amount, currency = 'USD') {
this.amount = amount;
this.currency = currency;
}
add(other) {
if (this.currency !== other.currency) throw new Error("Currency mismatch");
return new Money(this.amount + other.amount, this.currency);
}
}三、实战案例
3.1 案例一:识别电商系统中的坏味道
以下是一个电商系统中的订单处理代码,其中包含多种坏味道。请尝试识别它们。
public class OrderManager {
// 坏味道1:过大的类 - 这个类做了太多事情
public String processOrder(String customerId, String[] productIds,
int[] quantities, String shippingAddress,
String paymentMethod, String cardNumber,
String expiryDate, String cvv, boolean isGift,
String giftMessage, String couponCode) {
// 坏味道2:过长参数列
// 坏味道3:过长函数
// 验证客户
String customerData = db.query("SELECT * FROM customers WHERE id = '" + customerId + "'");
if (customerData == null) return "ERROR: Customer not found";
// 验证产品
double total = 0;
for (int i = 0; i < productIds.length; i++) {
String product = db.query("SELECT * FROM products WHERE id = '" + productIds[i] + "'");
if (product == null) return "ERROR: Product not found: " + productIds[i];
double price = Double.parseDouble(product.split(",")[2]);
total += price * quantities[i];
}
// 应用优惠券
if (couponCode != null && !couponCode.isEmpty()) {
String coupon = db.query("SELECT * FROM coupons WHERE code = '" + couponCode + "'");
if (coupon != null) {
double discount = Double.parseDouble(coupon.split(",")[1]);
total -= total * discount / 100;
}
}
// 计算运费
double shipping = 0;
if (total < 50) shipping = 10;
else if (total < 100) shipping = 5;
total += shipping;
// 处理支付
// 坏味道4:重复代码 - 支付逻辑在多处重复
if (paymentMethod.equals("CREDIT_CARD")) {
// 验证信用卡...
// 处理支付...
} else if (paymentMethod.equals("PAYPAL")) {
// 处理PayPal支付...
}
// 保存订单
String orderId = UUID.randomUUID().toString();
db.execute("INSERT INTO orders VALUES ('" + orderId + "', '" + customerId + "', " + total + ")");
// 发送邮件
// 坏味道5:神秘命名
sendMail(customerData, orderId, total);
return orderId;
}
// 坏味道6:霰弹式修改 - 订单相关逻辑分散在多个类中
public void cancelOrder(String orderId) { /* ... */ }
public void shipOrder(String orderId) { /* ... */ }
public void refundOrder(String orderId) { /* ... */ }
}3.2 案例二:代码审查中的坏味道检查清单
在实际工作中,可以使用以下检查清单来系统地识别坏味道:
## 代码审查检查清单
### 命名
- [ ] 函数名是否清晰表达了其用途?
- [ ] 变量名是否反映了数据的含义?
- [ ] 是否有缩写或拼写错误?
### 函数
- [ ] 函数是否超过 30 行?
- [ ] 函数是否做了多于一件事?
- [ ] 参数是否超过 4 个?
- [ ] 是否有 boolean 参数?
- [ ] 缩进层次是否超过 3 层?
### 类
- [ ] 类文件是否超过 500 行?
- [ ] 类是否有超过 20 个方法?
- [ ] 类是否因为多种原因被修改?
### 重复
- [ ] 是否有完全相同的代码块?
- [ ] 是否有结构相似但细节不同的代码?
- [ ] 是否有复制粘贴的代码?
### 依赖
- [ ] 一个修改是否需要改动多个文件?
- [ ] 一个类是否过于频繁地访问其他类的数据?四、本章小结
第3章系统地介绍了代码的坏味道:
神秘命名:名称没有揭示意图,增加理解成本
重复代码:相同的代码出现在多处,增加维护成本
过长函数:函数做了太多事,难以理解和测试
过长参数列:参数过多,容易传错,暗示职责过多
发散式变化:一个类因多种原因被修改,违反单一职责
霰弹式修改:一个变化需要修改多个类,增加遗漏风险
其他坏味道:过大的类、依恋情结、数据泥团、基本类型偏执等
本章的核心思想是:坏味道是代码在呼唤重构。学会识别这些信号,你就能在代码变得不可维护之前及时改善它。正如 Fowler 所说:"记住,编译器不会抱怨代码的坏味道,但下一个阅读代码的程序员会。"
五、思考与练习
思考题
坏味道与代码风格有什么区别?如何区分主观偏好和客观问题?
是否所有坏味道都需要立即修复?如何确定重构的优先级?
在敏捷开发中,如何平衡"快速交付"和"消除坏味道"?
如何在团队中建立识别坏味道的共同语言?
编程练习
在你当前的项目中,找出 3 个不同类型的坏味道,记录它们的特征和位置。
选择其中一个坏味道,使用对应的重构手法进行修复,并验证行为不变。
为团队编写一份"坏味道检查清单",用于代码审查时参考。