设计原则在重构中的应用:识别代码坏味道,逐步引入设计原则,重构案例实战
说实话,很多同学学完设计原则后,最大的困惑是——「我什么时候该用这些原则?」
原则背得滚瓜烂熟,打开项目一看,满屏的 if-else 和巨型类,完全不知道从哪下手。我个人习惯是:先别想原则,先找坏味道。坏味道是代码在「喊救命」,设计原则是「急救方案」。今天我们就拿一个真实案例,走一遍完整的重构流程。
1. 什么是代码坏味道?
坏味道不是 bug,但它预示着未来会出 bug。我总结了几种最常见的:
- 重复代码:同样的逻辑散落在各处
- 过长函数:一个函数超过 50 行,基本可以拆了
- 过大类:一个类做了太多不相关的事
- 散弹式修改:改一个需求,要改 5 个文件
- 依恋情结:一个函数总在访问别的类的数据
- 基本类型偏执:用 int 表示金额、用 string 表示状态
我的经验:如果你在代码 review 时觉得「这段代码看着别扭」,那大概率就是有坏味道。别犹豫,标记它。
2. 案例:一个订单处理系统
假设我们有一个订单处理类,它负责计算价格、验证库存、发送通知。先看看「坏味道版本」:
class OrderProcessor {
public:
void process(Order& order) {
// 计算总价
double total = 0;
for (auto& item : order.items) {
if (item.type == "BOOK") {
total += item.price * 0.9; // 书籍打9折
} else if (item.type == "ELECTRONICS") {
total += item.price * 0.95; // 电子产品打95折
} else {
total += item.price;
}
}
// 验证库存
for (auto& item : order.items) {
if (inventory[item.id] < item.quantity) {
throw std::runtime_error("库存不足");
}
}
// 发送邮件通知
EmailSender sender;
sender.send(order.customerEmail, "您的订单已处理");
// 记录日志
Logger::log("订单处理完成: " + order.id);
}
};
你看出几个坏味道了?
- 过长函数:process 函数干了四件事
- 重复代码:遍历订单项的逻辑重复了两次
- 基本类型偏执:商品类型用 string 表示,折扣逻辑用 if-else 硬编码
- 依恋情结:OrderProcessor 直接操作 inventory 和 EmailSender
3. 逐步引入设计原则
重构不是一蹴而就的。我建议分三步走:
第一步:单一职责原则 —— 拆函数
先把 process 拆成几个小函数。每个函数只做一件事:
class OrderProcessor {
public:
void process(Order& order) {
double total = calculateTotal(order);
validateInventory(order);
sendNotification(order);
logOrder(order);
}
private:
double calculateTotal(const Order& order) {
double total = 0;
for (auto& item : order.items) {
total += applyDiscount(item);
}
return total;
}
double applyDiscount(const Item& item) {
if (item.type == "BOOK") return item.price * 0.9;
if (item.type == "ELECTRONICS") return item.price * 0.95;
return item.price;
}
void validateInventory(const Order& order) { /* ... */ }
void sendNotification(const Order& order) { /* ... */ }
void logOrder(const Order& order) { /* ... */ }
};
注意:拆函数不是目的,目的是让每个函数有清晰的职责。以后改折扣逻辑,只需要改 applyDiscount,不会影响其他部分。
第二步:开闭原则 —— 用策略模式替换 if-else
applyDiscount 里的 if-else 还是坏味道。每新增一种商品类型,就要改这个函数。这违反了开闭原则(对扩展开放,对修改关闭)。
我引入策略模式:
class DiscountStrategy {
public:
virtual double apply(double price) = 0;
virtual ~DiscountStrategy() = default;
};
class BookDiscount : public DiscountStrategy {
public:
double apply(double price) override { return price * 0.9; }
};
class ElectronicsDiscount : public DiscountStrategy {
public:
double apply(double price) override { return price * 0.95; }
};
class NoDiscount : public DiscountStrategy {
public:
double apply(double price) override { return price; }
};
然后在 OrderProcessor 里维护一个策略映射表:
class OrderProcessor {
private:
std::unordered_map<std::string, std::unique_ptr<DiscountStrategy>> strategies;
public:
OrderProcessor() {
strategies["BOOK"] = std::make_unique<BookDiscount>();
strategies["ELECTRONICS"] = std::make_unique<ElectronicsDiscount>();
}
double applyDiscount(const Item& item) {
auto it = strategies.find(item.type);
if (it != strategies.end()) {
return it->second->apply(item.price);
}
return item.price;
}
};
避坑指南:我曾经在项目中把所有策略都写在一个工厂类里,结果工厂类越来越大。后来发现,用依赖注入的方式,把策略从外部传入,会更灵活。比如用配置文件或注册表来管理策略映射。
第三步:依赖倒置原则 —— 解耦外部依赖
OrderProcessor 直接依赖了 EmailSender 和 Logger 的具体实现。这导致单元测试很困难——每次测试都要真的发邮件。
引入接口:
class INotificationSender {
public:
virtual void send(const std::string& to, const std::string& message) = 0;
virtual ~INotificationSender() = default;
};
class ILogger {
public:
virtual void log(const std::string& message) = 0;
virtual ~ILogger() = default;
};
class OrderProcessor {
private:
std::unique_ptr<INotificationSender> notifier;
std::unique_ptr<ILogger> logger;
public:
OrderProcessor(std::unique_ptr<INotificationSender> n,
std::unique_ptr<ILogger> l)
: notifier(std::move(n)), logger(std::move(l)) {}
void process(Order& order) {
double total = calculateTotal(order);
validateInventory(order);
notifier->send(order.customerEmail, "您的订单已处理");
logger->log("订单处理完成: " + order.id);
}
};
现在测试时,可以传入 Mock 对象:
class MockNotifier : public INotificationSender {
public:
void send(const std::string&, const std::string&) override {
// 什么都不做,或者记录调用次数
}
};
4. 重构后的整体结构
用一张图来总结整个重构过程:
5. 重构的收益
重构完成后,我们看看对比:
| 维度 | 重构前 | 重构后 |
|---|---|---|
| 函数长度 | 50+ 行 | 每个函数 < 10 行 |
| 新增商品类型 | 修改 OrderProcessor | 新增策略类,无需修改现有代码 |
| 单元测试 | 困难(依赖具体实现) | 容易(可注入 Mock) |
| 代码重复 | 多次遍历订单项 | 每个职责只遍历一次 |
注意:不要为了用原则而用原则。如果项目只有 3 种商品类型,且永远不会变,那策略模式就是过度设计。我见过太多「为了模式而模式」的项目,最后代码比原来还难维护。
6. 总结
重构的核心思路其实很简单:
- 先闻味道:找到代码中「不对劲」的地方
- 小步重构:每次只改一个坏味道,保持测试通过
- 引入原则:用设计原则指导重构方向
- 验证效果:确保可测试性、可扩展性确实提升了
我个人习惯在每次代码 review 时,至少标记一个坏味道,然后花 15 分钟重构。积少成多,半年后你会发现代码质量有了质的飞跃。
记住:设计原则不是教条,是工具。用对了,代码会感谢你。