对象和数据结构:隐藏数据还是暴露行为?
《代码整洁之道》第六章对比了对象和数据结构的设计哲学,核心差异在于:对象隐藏数据,暴露行为;数据结构暴露数据,缺乏行为。这两种设计的选择,直接影响代码的可扩展性和维护性。
数据抽象:别暴露细节
好的抽象不会暴露数据的实现方式。比如表示平面上的点,糟糕的设计会直接暴露坐标:
// 反例:暴露实现细节
public class Point {
public double x;
public double y;
}
这种设计把点绑定在了直角坐标系上。如果业务需要切换到极坐标,所有使用x
和y
的代码都得改。而好的抽象会隐藏实现:
// 正例:抽象数据访问
public interface Point {
double getX();
double getY();
void setCartesian(double x, double y);
double getR(); // 极坐标半径
double getTheta(); // 极坐标角度
void setPolar(double r, double theta);
}
这样的接口既可以用直角坐标实现,也可以用极坐标实现,使用者无需关心内部存储方式。抽象的关键是只暴露做什么,不暴露怎么做。
对象与数据结构的“对立性”
对象和数据结构的设计方向完全相反:
- 对象:通过多态隐藏数据,新增类型时无需修改现有行为(符合开放-闭合原则)。比如形状计算面积:
// 对象风格:新增形状无需改AreaCalculator
public interface Shape {
double area();
}
public class Square implements Shape {
private double side;
public double area() { return side * side; }
}
public class AreaCalculator {
public double totalArea(Shape[] shapes) {
double sum = 0;
for (Shape s : shapes) sum += s.area();
return sum;
}
}
- 数据结构:暴露数据,新增行为时无需修改现有数据结构。比如用结构体存储形状数据:
// 数据结构风格:新增计算无需改数据
public class Square { public double side; }
public class Circle { public double radius; }
public class AreaCalculator {
public double area(Object shape) {
if (shape instanceof Square) {
Square s = (Square) shape;
return s.side * s.side;
} else if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.radius * c.radius;
}
throw new IllegalArgumentException();
}
}
核心矛盾:对象擅长新增类型,数据结构擅长新增行为。根据业务变化方向选择设计,才能减少修改成本。
得墨忒耳律:别跟陌生人说话
得墨忒耳律要求:模块不应访问其直接关联对象的内部结构。比如下面的代码就违反了这一原则:
// 反例:链式调用暴露内部结构
String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
这段代码知道ctxt
包含Options
,Options
包含ScratchDir
,一旦中间结构变化(比如Options
换了存储目录的方式),所有调用处都得改。优化方案是让ctxt
提供直接获取路径的方法:
// 正例:封装内部结构
public class Context {
private Options options;
public String getScratchDirPath() {
return options.getScratchDir().getAbsolutePath();
}
}
// 调用处无需关心内部结构
String outputDir = ctxt.getScratchDirPath();
本质是减少依赖:模块只应依赖直接合作的对象,不依赖对象的“朋友的朋友”。
数据传送对象(DTO)的正确用法
DTO是纯粹的数据容器,只包含公共字段(或getter/setter),无业务逻辑。最常见的是前后端交互的实体类:
// 典型DTO:仅用于数据传递
public class UserDTO {
private String name;
private int age;
// 仅getter和setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
DTO的价值在于高效传递数据,但不应在业务逻辑中使用。如果给DTO加业务方法(比如calculateScore()
),就会变成“半对象半数据”的混乱结构,既不适合扩展类型,也不适合扩展行为。
实践建议
-
根据变化方向选设计:
- 若频繁新增类型(如各种形状、支付方式),用对象(多态)。
- 若频繁新增行为(如各种统计、计算),用数据结构(暴露数据)。
-
避免“混合体”:
类要么隐藏数据暴露行为(对象),要么暴露数据无行为(数据结构),别搞“既有公共字段又有业务方法”的四不像。 -
DTO只做数据容器:
禁止在DTO中加业务逻辑,也别让业务逻辑依赖DTO的结构。
代码设计的本质是管理依赖:对象通过封装减少依赖范围,数据结构通过暴露数据降低扩展成本。理解这两种模式的适用场景,才能写出真正灵活的代码。