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

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


1. 开篇场景

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

1
2
3
4
5
6
7
8
9
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:逐步重构

重构前(问题代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 坏味道:两个类做类似的事情,但接口不同
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()
  • 使用这些类需要记住不同的接口

重构后(清洁版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ✅ 统一接口,使用多态
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)- 如何为只有数据的类添加行为