6代码整洁之道

对象和数据结构

数据抽象

数据抽象对象数据结构迪米特法则

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第6章 对象和数据结构

一、导读

1.1 本章学习目标

第6章深入讲解了对象和数据结构的设计原则。Uncle Bob 区分了对象(暴露行为,隐藏数据)和数据结构(暴露数据,没有显著行为)。本章的核心学习目标包括:

  • 理解数据抽象:隐藏实现细节。
  • 区分对象和数据结构:不同的设计选择。
  • 掌握迪米特法则:不要和陌生人说话。
  • 理解得墨忒耳定律:避免过长的方法链。
  • 学会封装的重要性:保护数据完整性。
  • 掌握何时使用对象,何时使用数据结构。

1.2 为什么本章如此重要

对象和数据结构的选择直接影响代码的耦合度和可维护性。Uncle Bob 指出:对象暴露行为,隐藏数据;数据结构暴露数据,没有显著行为。混淆两者会导致代码耦合度增加。

1.3 核心问题引导

对象和数据结构有什么区别?

什么是数据抽象?为什么重要?

迪米特法则是什么?如何应用?

什么时候应该用对象?什么时候用数据结构?

封装的目的是什么?


二、核心概念详解

2.1 数据抽象

2.1.1 隐藏实现细节

数据抽象意味着不暴露数据的内部表示,而是通过接口来操作数据。

java
// 差:暴露内部表示
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 对象:暴露行为,隐藏数据

java
// 对象:通过方法暴露行为
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 数据结构:暴露数据,没有显著行为

java
// 数据结构:暴露数据
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 选择的标准

  • 如果需要添加新的行为:使用对象(添加新方法即可)
  • 如果需要添加新的数据类型:使用数据结构(添加新类即可)
java
// 对象方式:容易添加新行为
public interface Shape {
  double area();
  // 添加新方法:perimeter()
  double perimeter();
}

// 数据结构方式:容易添加新类型
public class Circle {
  public double radius;
}
public class Rectangle {
  public double width;
  public double height;
}
// 添加新类型:Triangle

2.3 迪米特法则

2.3.1 不要和陌生人说话

迪米特法则(Law of Demeter):一个对象应该只与直接的朋友通信,不应该了解其朋友的细节。

java
// 差:违反迪米特法则(过长的方法链)
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 封装的目的

封装的目的不仅是"隐藏数据",更是:

  • 保护数据完整性:防止外部代码随意修改数据
  • 隐藏实现细节:内部实现可以随意修改
  • 提供清晰的接口:让使用者只关心"做什么"
java
// 差:没有封装
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 案例一:从数据结构到对象

java
// 改善前:纯数据结构
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 的核心观点是:理解对象和数据结构的区别,是设计低耦合代码的关键。


五、思考与练习

思考题

你的代码中有哪些是对象?哪些是数据结构?

有没有违反迪米特法则的地方?

封装在你的代码中做得如何?

编程练习

将一个暴露数据的数据结构改造为封装良好的对象。

找到一段过长的方法链,用迪米特法则改善。

为一个"图书馆管理系统"设计对象和数据结构。

6/10对象和数
0%