八、结构化信息传递的高效方式

8 阅读4分钟

对象和数据结构:隐藏数据还是暴露行为?

《代码整洁之道》第六章对比了对象和数据结构的设计哲学,核心差异在于:对象隐藏数据,暴露行为;数据结构暴露数据,缺乏行为。这两种设计的选择,直接影响代码的可扩展性和维护性。

数据抽象:别暴露细节

好的抽象不会暴露数据的实现方式。比如表示平面上的点,糟糕的设计会直接暴露坐标:

// 反例:暴露实现细节
public class Point {
    public double x;
    public double y;
}

这种设计把点绑定在了直角坐标系上。如果业务需要切换到极坐标,所有使用xy的代码都得改。而好的抽象会隐藏实现:

// 正例:抽象数据访问
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包含OptionsOptions包含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()),就会变成“半对象半数据”的混乱结构,既不适合扩展类型,也不适合扩展行为。

实践建议

  1. 根据变化方向选设计

    • 若频繁新增类型(如各种形状、支付方式),用对象(多态)。
    • 若频繁新增行为(如各种统计、计算),用数据结构(暴露数据)。
  2. 避免“混合体”
    类要么隐藏数据暴露行为(对象),要么暴露数据无行为(数据结构),别搞“既有公共字段又有业务方法”的四不像。

  3. DTO只做数据容器
    禁止在DTO中加业务逻辑,也别让业务逻辑依赖DTO的结构。

代码设计的本质是管理依赖:对象通过封装减少依赖范围,数据结构通过暴露数据降低扩展成本。理解这两种模式的适用场景,才能写出真正灵活的代码。