33. 设计模式与代码重构:用设计模式消除重复代码,改善可维护性,实际重构案例
说实话,我面试过不少候选人,简历上写着「精通设计模式」,但一聊到实际重构,就只会背那几个模式的定义。真正让我眼前一亮的,是那些能指着一段烂代码说:「这里用策略模式重构,能砍掉一半重复代码」的人。
今天咱们就聊点实在的——怎么用设计模式去消除重复代码,改善可维护性。我会拿一个我实际重构过的案例来讲,你跟着走一遍,应该能感受到那种「代码突然变清爽了」的快感。
先看一段「坏味道」代码
假设我们有个订单处理系统。不同订单类型,处理逻辑不一样。我见过有人这么写:
class OrderProcessor {
public:
void process(Order& order) {
if (order.type() == "normal") {
// 普通订单:检查库存、扣减、发货
checkStock(order);
deductStock(order);
ship(order);
} else if (order.type() == "gift") {
// 礼品订单:检查库存、扣减、包装、发货
checkStock(order);
deductStock(order);
wrapGift(order);
ship(order);
} else if (order.type() == "preorder") {
// 预购订单:检查库存、扣减、标记预购、发货
checkStock(order);
deductStock(order);
markPreorder(order);
ship(order);
}
// 以后每加一种类型,这里就多一个 else if
}
};
这段代码有什么问题?你一眼就能看出来:重复。每个分支里都有 checkStock、deductStock、ship。而且,如果未来要加「国际订单」「团购订单」,这个函数会膨胀到没法看。
我在项目中遇到过类似的情况,当时那个文件已经 2000 多行了。每次改一个逻辑,都得小心翼翼,生怕漏掉某个分支。嗯,这就是典型的「坏味道」——重复的 if-else 结构。
用策略模式消除重复
为什么会这样?说白了,就是不同的处理逻辑被硬编码在同一个函数里了。我们需要的,是把「变化的部分」抽出来,让「不变的部分」复用。
策略模式正好干这个事。它把每个算法封装成独立的类,让它们可以互相替换。来看看重构后的样子:
// 策略接口
class OrderStrategy {
public:
virtual ~OrderStrategy() = default;
virtual void execute(Order& order) = 0;
};
// 具体策略:普通订单
class NormalOrderStrategy : public OrderStrategy {
public:
void execute(Order& order) override {
checkStock(order);
deductStock(order);
ship(order);
}
};
// 具体策略:礼品订单
class GiftOrderStrategy : public OrderStrategy {
public:
void execute(Order& order) override {
checkStock(order);
deductStock(order);
wrapGift(order);
ship(order);
}
};
// 具体策略:预购订单
class PreorderStrategy : public OrderStrategy {
public:
void execute(Order& order) override {
checkStock(order);
deductStock(order);
markPreorder(order);
ship(order);
}
};
// 上下文类
class OrderProcessor {
public:
void process(Order& order) {
auto strategy = getStrategy(order.type());
strategy->execute(order);
}
private:
std::unique_ptr<OrderStrategy> getStrategy(const std::string& type) {
if (type == "normal") return std::make_unique<NormalOrderStrategy>();
if (type == "gift") return std::make_unique<GiftOrderStrategy>();
if (type == "preorder") return std::make_unique<PreorderStrategy>();
throw std::runtime_error("unknown order type");
}
};
你看,OrderProcessor::process 现在只有三行。每个策略类只关心自己的特殊逻辑。以后加新类型,只需要新增一个策略类,改一下 getStrategy 工厂方法就行。原来的代码不用动。
核心思想:把「变」与「不变」分离。不变的流程(检查库存、扣减、发货)可以提取到基类或模板方法里,变的细节(包装、标记)交给子类。
再进一步:用模板方法模式消除策略内的重复
你仔细看上面的策略类,每个策略里都有 checkStock、deductStock、ship 这三步。这不又重复了吗?
嗯,这里可以用模板方法模式来优化。把公共流程放到基类,让子类只实现差异部分:
class OrderStrategy {
public:
virtual ~OrderStrategy() = default;
// 模板方法:定义了算法骨架
void execute(Order& order) {
checkStock(order);
deductStock(order);
extraProcess(order); // 子类实现
ship(order);
}
protected:
virtual void extraProcess(Order& order) = 0;
private:
void checkStock(Order& order) { /* 公共实现 */ }
void deductStock(Order& order) { /* 公共实现 */ }
void ship(Order& order) { /* 公共实现 */ }
};
class GiftOrderStrategy : public OrderStrategy {
protected:
void extraProcess(Order& order) override {
wrapGift(order);
}
};
class PreorderStrategy : public OrderStrategy {
protected:
void extraProcess(Order& order) override {
markPreorder(order);
}
};
这样一来,重复的公共代码被彻底消除。每个子类只需要写自己那一点点特殊逻辑。我个人习惯把这种组合叫做「策略+模板方法」双模式,对付这种「流程固定、细节多变」的场景特别好用。
重构的收益:可维护性大幅提升
重构完以后,你想想看,如果产品经理说:「我们要加一个国际订单,需要额外做汇率转换和海关申报。」
你只需要:
- 新建一个
InternationalOrderStrategy,继承OrderStrategy - 在
extraProcess里实现汇率转换和海关申报 - 在
getStrategy里加一个分支
原来的代码一行都不用改。这就是开闭原则——对扩展开放,对修改关闭。
避坑指南:我曾经在重构时犯过一个错——把所有策略类都塞进一个文件里。结果文件又变成了 1000 多行。记住,每个策略类单独一个文件,命名清晰,比如 gift_order_strategy.h。这样找代码、改代码都方便。
知识体系:设计模式与重构的关系
为了让你更直观地理解,我画了一张图,展示设计模式在重构中的定位:
这张图想表达的是:坏味道 → 重构手法 → 设计模式 → 收益。你不需要记住所有模式,但你要能识别出代码里的坏味道,然后知道用哪种模式去解决。
实际重构中的几个建议
最后,分享几个我这些年重构踩过的坑:
- 不要一次性重构太多。我曾经试图在一个大版本里把所有 if-else 都改成策略模式,结果改到一半需求变了,代码变得不伦不类。建议每次只重构一个模块,测试通过后再继续。
- 先写测试,再重构。没有测试的保护,重构就是裸奔。我习惯先用单元测试把现有行为锁定,然后放心大胆地改。
- 模式不是银弹。如果只有两三种分支,用 if-else 反而更清晰。别为了用模式而用模式,那叫过度设计。
- 注意策略类的生命周期。如果策略类是无状态的,可以做成单例,避免每次创建新对象。我在一个高并发系统里就吃过这个亏——频繁 new 策略对象导致 GC 压力大。
警告:重构时最容易犯的错是「改着改着就偏离了原始行为」。我曾经把一个订单处理逻辑重构后,发现礼品订单不再扣减库存了——因为我在提取公共流程时漏掉了 deductStock。所以,重构后一定要跑一遍所有测试用例,尤其是边界情况。
好了,关于设计模式与代码重构,今天就聊这么多。记住,模式是工具,不是目的。你的目标是写出容易改、不容易出 bug 的代码。下次你看到一段重复的 if-else,不妨想想:这里能不能用策略模式?能不能用模板方法?