异曲同工的类(Alternative Classes with Different Interfaces):坏味道识别与重构实战指南

24种代码坏味道系列 · 第21篇


1. 开篇场景

你是否遇到过这样的代码:Rectangle 类使用 computeArea()computePerimeter(),而 Box 类使用 area()perimeter(),虽然功能相同,但接口不同,使用时需要记住不同的方法名?

class Rectangle {
    double computeArea() const { return width * height; }
    double computePerimeter() const { return 2 * (width + height); }
};

class Box {
    double area() const { return length * breadth; }
    double perimeter() const { return 2 * (length + breadth); }
};

这就是异曲同工的类的典型症状。功能相似的类使用不同的接口,增加了使用复杂度,就像同一首歌的不同版本,虽然旋律相同,但歌词不同,需要记住不同的版本。

当你需要使用这些类时,你必须记住不同的接口。当你需要添加新功能时,你必须在多个类中实现。这种设计使得代码变得复杂,增加了维护的难度。


2. 坏味道定义

异曲同工的类是指功能相似的类使用不同的接口,增加了使用复杂度。

就像同一首歌的不同版本,虽然旋律相同,但歌词不同,需要记住不同的版本。

核心问题:功能相似的类应该使用统一的接口。如果类功能相似但接口不同,应该统一接口,使用多态或统一命名。


3. 识别特征

🔍 代码表现:

  • 特征1:多个类功能相似但接口不同
  • 特征2:类的方法名不同但功能相同
  • 特征3:使用这些类需要记住不同的接口
  • 特征4:添加新功能时需要在多个类中实现
  • 特征5:类的参数类型或顺序不同

🎯 出现场景:

  • 场景1:多个开发者独立实现相似功能
  • 场景2:从其他代码复制时,保留了不同的接口
  • 场景3:缺乏设计,没有考虑接口的统一
  • 场景4:重构不彻底,只修改了部分代码

💡 快速自检:

  • 问自己:这些类是否功能相似但接口不同?
  • 问自己:是否可以统一接口?
  • 工具提示:使用代码分析工具检测功能相似但接口不同的类

4. 危害分析

🚨 维护成本:添加新功能需要在多个类中实现,时间成本增加50%

⚠️ 缺陷风险:接口不一致容易导致使用错误,bug风险增加40%

🧱 扩展障碍:添加新功能时不知道应该使用哪个接口

🤯 认知负担:需要记住不同的接口,增加了心理负担


5. 重构实战

步骤1:安全准备

  • ✅ 确保有完整的单元测试覆盖
  • ✅ 创建重构分支:git checkout -b refactor/unify-interfaces
  • ✅ 使用版本控制,便于回滚

步骤2:逐步重构

重构前(问题代码)

// 坏味道:两个类做类似的事情,但接口不同
class Rectangle {
private:
    double width;
    double height;
    
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    
    double getWidth() const { return width; }
    double getHeight() const { return height; }
    void setWidth(double w) { width = w; }
    void setHeight(double h) { height = h; }
    
    double computeArea() const {
        return width * height;
    }
    
    double computePerimeter() const {
        return 2 * (width + height);
    }
};

class Box {
private:
    double length;
    double breadth;
    
public:
    Box(double l, double b) : length(l), breadth(b) {}
    
    double getLength() const { return length; }
    double getBreadth() const { return breadth; }
    void setLength(double l) { length = l; }
    void setBreadth(double b) { breadth = b; }
    
    double area() const {
        return length * breadth;
    }
    
    double perimeter() const {
        return 2 * (length + breadth);
    }
};

问题分析

  • RectangleBox 功能相似但接口不同
  • computeArea() vs area()computePerimeter() vs perimeter()
  • 使用这些类需要记住不同的接口

重构后(清洁版本)

// ✅ 统一接口,使用多态
class Shape {
public:
    virtual double getArea() const = 0;
    virtual double getPerimeter() const = 0;
    virtual ~Shape() = default;
};

class GoodRectangle : public Shape {
private:
    double width;
    double height;
    
public:
    GoodRectangle(double w, double h) : width(w), height(h) {}
    
    double getWidth() const { return width; }
    double getHeight() const { return height; }
    void setWidth(double w) { width = w; }
    void setHeight(double h) { height = h; }
    
    // ✅ 统一接口
    double getArea() const override {
        return width * height;
    }
    
    double getPerimeter() const override {
        return 2 * (width + height);
    }
};

关键变化点

  1. 统一接口(Unify Interfaces)

    • 使用多态统一接口
    • 所有类实现相同的接口
  2. 提高可维护性

    • 使用统一的接口,代码更易维护
    • 添加新功能只需实现接口
  3. 提高可扩展性

    • 添加新的形状类只需实现接口
    • 不需要修改现有代码

步骤3:重构技巧总结

使用的重构手法

  • 统一接口(Unify Interfaces):将功能相似的类统一接口
  • 提取超类(Extract Superclass):提取公共接口到超类

注意事项

  • ⚠️ 确保接口设计合理
  • ⚠️ 如果类功能差异较大,考虑使用组合
  • ⚠️ 重构后要更新所有使用处,确保行为一致

6. 预防策略

🛡️ 编码时:

  • 即时检查

    • 类是否功能相似但接口不同?
    • 是否可以统一接口?
    • 使用IDE的代码分析工具,检测功能相似但接口不同的类
  • 小步提交

    • 发现异曲同工的类时,立即统一接口
    • 使用”统一接口”重构,保持接口一致

🔍 Code Review清单:

  • 重点检查

    • 是否有功能相似但接口不同的类?
    • 是否可以统一接口?
    • 使用这些类是否需要记住不同的接口?
  • 拒绝标准

    • 功能相似但接口不同的类
    • 类的方法名不同但功能相同
    • 使用这些类需要记住不同的接口

⚙️ 自动化防护:

  • IDE配置

    • 使用代码分析工具检测功能相似但接口不同的类
    • 启用接口一致性警告
  • CI/CD集成

    • 在CI流水线中集成代码分析工具
    • 检测功能相似但接口不同的类,生成警告报告

下一篇预告:纯数据类(Data Class)- 如何为只有数据的类添加行为