数据抽象 (Data Abstraction)
这个小节主要讲的是**面向对象编程(OOP)**的一种核心思想:对象应该隐藏它的内部数据,只暴露可以操作这些数据的"行为"(也就是方法/函数)。
- 大白话: 你创建一个"用户"对象,这个对象内部可能存着用户的姓名、年龄、地址等数据。但在好的面向对象设计里,你不应该让外部代码直接去随意修改这些数据(比如
user.name = "新的名字"; user.age = -10;
)。
-
- 相反,你应该给"用户"对象提供一些方法,比如
user.setName("新的名字");
或user.setAge(30);
。在这些方法里面,你可以控制数据的有效性(比如检查年龄不能是负数),或者做一些附带的操作(比如修改姓名时记录日志)。
- 相反,你应该给"用户"对象提供一些方法,比如
- 核心: 对象不仅仅是数据的容器,它还是数据和操作数据的行为的结合体。它就像一个黑箱,外面的人不知道它里面是怎么存数据的,只能通过它提供的有限的几个按钮(方法)来和它交互。
- 为什么重要: 这是为了隐藏实现细节 。如果将来你决定改变用户数据在对象内部的存储方式(比如原来用字符串存地址,现在改成一个 Address 对象),只要
setName()
、setAge()
等方法签名不变,外部调用这些方法的代码就不需要修改。这让你的代码更容易修改和演进。
数据/对象反模式
这个小节是整个第六章最核心、也可能最让人困惑的地方。它是在对比面向对象那种"隐藏数据、暴露行为"的方式,与另一种**"暴露数据、用过程/函数操作数据"的方式**。
- 面向对象风格: 隐藏数据,暴露行为。优点: 易于添加新的对象类型 (不改方法)。缺点: 难于添加新的行为(要改所有相关类)。目前我开发都用这种风格
- 数据结构风格: 暴露数据(public 变量或简单 getter),将操作数据的行为放在外部函数/类 中。优点: 易于添加新的行为 (新增外部函数)。缺点: 难于添加新的数据结构类型(要改所有相关的外部函数)。
数据结构风格代码
java
// ShapeData.java
// 圆的数据结构
class CircleData {
public double radius; // 公开暴露半径数据
public CircleData(double radius) {
this.radius = radius;
}
}
// 正方形的数据结构
class SquareData {
public double side; // 公开暴露边长数据
public SquareData(double side) {
this.side = side;
}
}
// 其他形状的数据结构...
// class TriangleData { public double base; public double height; ... }
java
// ShapeCalculator.java
class ShapeCalculator {
// 计算圆的面积的函数
public static double calculateArea(CircleData circle) {
// 直接访问 CircleData 的公开数据
return Math.PI * circle.radius * circle.radius;
}
// 计算正方形的面积的函数
public static double calculateArea(SquareData square) {
// 直接访问 SquareData 的公开数据
return square.side * square.side;
}
// 如果需要处理不同类型的形状,可能会有这样的函数,里面包含判断逻辑
// 注意:这种函数在增加新的形状类型时需要修改
public static double calculateArea(Object shape) {
if (shape instanceof CircleData) {
CircleData circle = (CircleData) shape;
return Math.PI * circle.radius * circle.radius;
} else if (shape instanceof SquareData) {
SquareData square = (SquareData) shape;
return square.side * square.side;
}
// 如果有新的形状类型 (比如 TriangleData),这里就需要加新的 if/else
throw new IllegalArgumentException("Unknown shape type");
}
// 如果需要添加新的操作 (比如计算周长),只需要在这里添加新的函数
public static double calculatePerimeter(CircleData circle) {
return 2 * Math.PI * circle.radius;
}
public static double calculatePerimeter(SquareData square) {
return 4 * square.side;
}
}
使用方式:
java
// Main.java
public class Main {
public static void main(String[] args) {
CircleData myCircle = new CircleData(5.0);
SquareData mySquare = new SquareData(4.0);
// 调用外部函数来计算面积
double circleArea = ShapeCalculator.calculateArea(myCircle);
double squareArea = ShapeCalculator.calculateArea(mySquare);
System.out.println("圆的面积: " + circleArea);
System.out.println("正方形的面积: " + squareArea);
// 使用通用计算函数 (需要 instanceof 判断)
double unknownShapeArea = ShapeCalculator.calculateArea((Object) mySquare);
System.out.println("未知形状面积 (正方形): " + unknownShapeArea);
// 调用外部函数来计算周长
double circlePerimeter = ShapeCalculator.calculatePerimeter(myCircle);
double squarePerimeter = ShapeCalculator.calculatePerimeter(mySquare);
System.out.println("圆的周长: " + circlePerimeter);
System.out.println("正方形的周长: " + squarePerimeter);
}
}
- 优点: 非常容易添加新的操作(函数) 。就像上面例子中,我们很方便地新增了
calculatePerimeter
函数来计算周长,而不需要修改CircleData
或SquareData
类本身。当你有很多种操作要应用于相对稳定的数据结构时,这种方式很方便。 - 缺点: 很难添加新的数据结构类型 。如果现在要加入一个
TriangleData
(三角形)数据结构,你需要修改所有那些需要处理形状的函数 (比如ShapeCalculator
中的calculateArea(Object shape)
就需要添加处理TriangleData
的逻辑),为新的形状类型添加相应的处理分支。
大白话,加一个新的数据结构 TriangleData
,那么 ShapeCalculator
类要做大量改动
得墨忒耳定律
不要链式调用, 如 a.getB().getC().doSomething()
。
直接获取对象调用方法
数据传输对象(DTOs)
DTO (Data Transfer Object): 数据传输对象。这是一种典型的数据结构 。 里面没有任何业务逻辑代码。它的唯一作用就是在不同的软件层次之间(比如从数据库层到服务层,或者从服务层到外部接口)传输数据。