设计原则在重构中的应用:识别代码坏味道,逐步引入设计原则,重构案例实战

说实话,很多同学学完设计原则后,最大的困惑是——「我什么时候该用这些原则?」

原则背得滚瓜烂熟,打开项目一看,满屏的 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. 重构后的整体结构

用一张图来总结整个重构过程:

重构过程:从坏味道到设计原则 代码坏味道 过长函数 · 重复代码 单一职责原则 拆函数 · 职责分离 开闭原则 策略模式 · 消除if-else 依赖倒置原则 接口抽象 · 依赖注入 可测试 · 可扩展 低耦合 · 高内聚 重构不是一步到位,而是逐步引入设计原则 🔍 识别坏味道 → 拆解 → 抽象 → 解耦 💡 每个原则解决一个具体问题,不要过度设计

5. 重构的收益

重构完成后,我们看看对比:

维度 重构前 重构后
函数长度 50+ 行 每个函数 < 10 行
新增商品类型 修改 OrderProcessor 新增策略类,无需修改现有代码
单元测试 困难(依赖具体实现) 容易(可注入 Mock)
代码重复 多次遍历订单项 每个职责只遍历一次
注意:不要为了用原则而用原则。如果项目只有 3 种商品类型,且永远不会变,那策略模式就是过度设计。我见过太多「为了模式而模式」的项目,最后代码比原来还难维护。

6. 总结

重构的核心思路其实很简单:

  1. 先闻味道:找到代码中「不对劲」的地方
  2. 小步重构:每次只改一个坏味道,保持测试通过
  3. 引入原则:用设计原则指导重构方向
  4. 验证效果:确保可测试性、可扩展性确实提升了

我个人习惯在每次代码 review 时,至少标记一个坏味道,然后花 15 分钟重构。积少成多,半年后你会发现代码质量有了质的飞跃。

记住:设计原则不是教条,是工具。用对了,代码会感谢你。


专注资料整理