第6章 对象和数据结构
一、导读
1.1 本章学习目标
第6章深入讲解了对象和数据结构的设计原则。Uncle Bob 区分了对象(暴露行为,隐藏数据)和数据结构(暴露数据,没有显著行为)。本章的核心学习目标包括:
- 理解数据抽象:隐藏实现细节。
- 区分对象和数据结构:不同的设计选择。
- 掌握迪米特法则:不要和陌生人说话。
- 理解得墨忒耳定律:避免过长的方法链。
- 学会封装的重要性:保护数据完整性。
- 掌握何时使用对象,何时使用数据结构。
1.2 为什么本章如此重要
对象和数据结构的选择直接影响代码的耦合度和可维护性。Uncle Bob 指出:对象暴露行为,隐藏数据;数据结构暴露数据,没有显著行为。混淆两者会导致代码耦合度增加。
1.3 核心问题引导
对象和数据结构有什么区别?
什么是数据抽象?为什么重要?
迪米特法则是什么?如何应用?
什么时候应该用对象?什么时候用数据结构?
封装的目的是什么?
二、核心概念详解
2.1 数据抽象
2.1.1 隐藏实现细节
数据抽象意味着不暴露数据的内部表示,而是通过接口来操作数据。
// 差:暴露内部表示
public class Point {
public double x;
public double y;
// 任何人都可以直接修改 x 和 y
}
// 好:数据抽象
public class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public double distanceTo(Point other) {
return Math.sqrt(
Math.pow(x - other.x, 2) +
Math.pow(y - other.y, 2)
);
}
// 内部表示可以随意修改
// 比如改为极坐标,不影响外部代码
}2.2 对象 vs 数据结构
2.2.1 对象:暴露行为,隐藏数据
// 对象:通过方法暴露行为
public class BankAccount {
private Money balance; // 数据隐藏
// 暴露行为
public void deposit(Money amount) {
if (amount.isNegative()) {
throw new IllegalArgumentException("Cannot deposit negative");
}
balance = balance.add(amount);
}
public void withdraw(Money amount) {
if (amount.greaterThan(balance)) {
throw new InsufficientFundsException();
}
balance = balance.subtract(amount);
}
public Money getBalance() {
return balance; // 返回不可变对象
}
}2.2.2 数据结构:暴露数据,没有显著行为
// 数据结构:暴露数据
public class Address {
public String street;
public String city;
public String state;
public String zipCode;
// 没有显著行为,只是数据的容器
}
// 使用数据结构
public class ShippingCalculator {
public Money calculate(Address address, Weight weight) {
// 直接访问数据
if (address.state.equals("CA")) {
return weight.multiply(0.1);
}
// ...
}
}2.2.3 选择的标准
- 如果需要添加新的行为:使用对象(添加新方法即可)
- 如果需要添加新的数据类型:使用数据结构(添加新类即可)
// 对象方式:容易添加新行为
public interface Shape {
double area();
// 添加新方法:perimeter()
double perimeter();
}
// 数据结构方式:容易添加新类型
public class Circle {
public double radius;
}
public class Rectangle {
public double width;
public double height;
}
// 添加新类型:Triangle2.3 迪米特法则
2.3.1 不要和陌生人说话
迪米特法则(Law of Demeter):一个对象应该只与直接的朋友通信,不应该了解其朋友的细节。
// 差:违反迪米特法则(过长的方法链)
public class OrderService {
public void processOrder(Order order) {
// 深入了解 order 的内部结构
String customerName = order.getCustomer().getName();
String email = order.getCustomer().getEmail();
String street = order.getShippingAddress().getStreet();
String city = order.getShippingAddress().getCity();
}
}
// 好:遵守迪米特法则
public class OrderService {
public void processOrder(Order order) {
// 只与 order 直接交互
String customerName = order.getCustomerName();
String email = order.getCustomerEmail();
String shippingInfo = order.getShippingInfo();
}
}2.4 封装
2.4.1 封装的目的
封装的目的不仅是"隐藏数据",更是:
- 保护数据完整性:防止外部代码随意修改数据
- 隐藏实现细节:内部实现可以随意修改
- 提供清晰的接口:让使用者只关心"做什么"
// 差:没有封装
public class User {
public String name;
public String email;
public String password; // 密码明文暴露!
}
// 好:良好封装
public class User {
private final String name;
private final String email;
private final EncryptedPassword password;
public User(String name, String email, String rawPassword) {
this.name = Objects.requireNonNull(name);
this.email = validateEmail(email);
this.password = EncryptedPassword.encrypt(rawPassword);
}
// 只读访问
public String getName() { return name; }
public String getEmail() { return email; }
// 行为:验证密码
public boolean verifyPassword(String rawPassword) {
return password.matches(rawPassword);
}
// 内部实现可以修改
private String validateEmail(String email) {
if (!email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
return email;
}
}三、实战案例
3.1 案例一:从数据结构到对象
// 改善前:纯数据结构
public class Order {
public List<Item> items;
public double totalAmount;
public String status;
// 所有逻辑都在外部
}
// 外部代码直接操作数据
public void processOrder(Order order) {
double total = 0;
for (Item item : order.items) {
total += item.price * item.quantity;
}
order.totalAmount = total;
order.status = "PROCESSED";
}
// 改善后:面向对象
public class Order {
private final List<OrderItem> items;
private Money totalAmount;
private OrderStatus status;
public Order(List<OrderItem> items) {
this.items = new ArrayList<>(items);
this.status = OrderStatus.CREATED;
recalculateTotal();
}
public Money getTotalAmount() { return totalAmount; }
public OrderStatus getStatus() { return status; }
public void confirm() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("Cannot confirm");
}
this.status = OrderStatus.CONFIRMED;
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(item -> item.getPrice().multiply(item.getQuantity()))
.reduce(Money.ZERO, Money::add);
}
}四、本章小结
第6章讲解了对象和数据结构的设计原则,核心要点包括:
数据抽象:隐藏实现细节,暴露行为接口。
对象 vs 数据结构:对象暴露行为隐藏数据,数据结构暴露数据。
迪米特法则:不要和陌生人说话,避免过长的方法链。
封装:保护数据完整性,隐藏实现细节。
选择标准:需要新行为用对象,需要新数据类型用数据结构。
Uncle Bob 的核心观点是:理解对象和数据结构的区别,是设计低耦合代码的关键。
五、思考与练习
思考题
你的代码中有哪些是对象?哪些是数据结构?
有没有违反迪米特法则的地方?
封装在你的代码中做得如何?
编程练习
将一个暴露数据的数据结构改造为封装良好的对象。
找到一段过长的方法链,用迪米特法则改善。
为一个"图书馆管理系统"设计对象和数据结构。