6代码大全(第2版)

可以工作的类

类的设计

类封装继承

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

第6章 可以工作的类

一、导读

1.1 本章学习目标

第6章深入讲解了面向对象编程中类的设计原则和实践。McConnell 详细介绍了如何创建高质量的类。本章的核心学习目标包括:

  • 掌握抽象的本质:提取本质特征,忽略无关细节。
  • 理解封装的重要性:隐藏实现细节,保护数据完整性。
  • 学会正确使用继承:is-a 关系的判断,避免继承滥用。
  • 掌握类的接口设计:简洁、一致、易用。
  • 理解类的内聚性:单一职责原则的应用。
  • 避免过度设计:简单优先,不要为假想的未来做设计。

1.2 为什么本章如此重要

类是面向对象编程的基本构建块。类的设计质量直接决定了整个系统的可维护性、可扩展性和可复用性。McConnell 指出:好的类设计让代码易于理解、易于修改、易于测试;糟糕的类设计让代码难以理解、难以修改、难以测试。

本章的重要性在于它提供了类设计的系统化方法。不是凭直觉设计类,而是基于明确的原则和实践。你会理解为什么有些类设计得好,有些设计得差,以及如何改善类的设计。

1.3 核心问题引导

什么是抽象?如何在类设计中正确地抽象?

封装的目的是什么?如何在实际编码中实现封装?

继承是"银弹"吗?什么时候应该使用继承,什么时候应该使用组合?

类的接口应该如何设计?如何做到简洁又完整?

如何判断一个类的职责是否过多?如何拆分?


二、核心概念详解

2.1 抽象

2.1.1 抽象的本质

抽象是提取事物的本质特征,忽略无关细节的过程。在类设计中,抽象意味着:

  • 提取核心概念:识别问题域中的关键实体。
  • 定义核心行为:确定实体应该做什么。
  • 隐藏实现细节:不关心具体怎么做。
java
// 抽象的示例:银行账户

// 核心概念:银行账户
// 核心行为:存款、取款、查询余额
// 隐藏细节:如何持久化、如何计算利息、如何处理并发

public abstract class BankAccount {
  // 抽象:定义核心行为
  public abstract void deposit(Money amount);
  public abstract void withdraw(Money amount);
  public abstract Money getBalance();
  
  // 抽象:定义核心属性
  public abstract AccountId getId();
  public abstract AccountHolder getHolder();
  
  // 隐藏:不暴露内部实现
  // 不暴露:交易记录的具体存储方式
  // 不暴露:利息计算的具体算法
  // 不暴露:并发控制的具体机制
}

// 具体实现可以有多种
public class SavingsAccount extends BankAccount {
  private Money balance;
  private InterestRate interestRate;
  
  @Override
  public void deposit(Money amount) {
    if (amount.isNegative()) {
      throw new IllegalArgumentException("Cannot deposit negative amount");
    }
    balance = balance.add(amount);
  }
  
  @Override
  public void withdraw(Money amount) {
    if (amount.greaterThan(balance)) {
      throw new InsufficientFundsException("Insufficient balance");
    }
    balance = balance.subtract(amount);
  }
  
  @Override
  public Money getBalance() {
    return balance;
  }
}

2.1.2 抽象的层次

抽象不是一次性的,而是有层次的:

java
// 层次1:最高抽象
public interface Account {
  void deposit(Money amount);
  void withdraw(Money amount);
  Money getBalance();
}

// 层次2:中间抽象
public abstract class AbstractBankAccount implements Account {
  private final AccountId id;
  private final AccountHolder holder;
  
  protected AbstractBankAccount(AccountId id, AccountHolder holder) {
    this.id = id;
    this.holder = holder;
  }
  
  // 模板方法:定义算法骨架
  public final void transferTo(Account target, Money amount) {
    validateTransfer(amount);
    this.withdraw(amount);
    target.deposit(amount);
    recordTransaction(amount);
  }
  
  protected abstract void validateTransfer(Money amount);
  protected abstract void recordTransaction(Money amount);
}

// 层次3:具体实现
public class CheckingAccount extends AbstractBankAccount {
  private Money balance;
  private double overdraftLimit;
  
  @Override
  protected void validateTransfer(Money amount) {
    if (amount.greaterThan(balance.add(overdraftLimit))) {
      throw new InsufficientFundsException();
    }
  }
  
  @Override
  protected void recordTransaction(Money amount) {
    // 记录到交易日志
  }
}

2.2 封装

2.2.1 封装的目的

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

  • 保护数据完整性:防止外部代码随意修改数据。
  • 隐藏实现细节:内部实现可以随意修改。
  • 提供清晰的接口:让使用者只关心"做什么"而非"怎么做"。
java
// 封装的示例:日期范围

// 差:暴露内部数据
public class BadDateRange {
  public Date start;
  public Date end;
  // 任何人都可以随意修改 start 和 end
  // 无法保证 start <= end
}

// 好:封装保护数据完整性
public class GoodDateRange {
  private final Date start;
  private final Date end;
  
  public GoodDateRange(Date start, Date end) {
    // 在构造时验证
    if (start.after(end)) {
      throw new IllegalArgumentException("Start must be before end");
    }
    // 防御性复制
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());
  }
  
  // 只读访问
  public Date getStart() {
    return new Date(start.getTime()); // 返回副本
  }
  
  public Date getEnd() {
    return new Date(end.getTime()); // 返回副本
  }
  
  // 暴露行为而非数据
  public boolean contains(Date date) {
    return !date.before(start) && !date.after(end);
  }
  
  public int getDurationInDays() {
    long diff = end.getTime() - start.getTime();
    return (int) TimeUnit.MILLISECONDS.toDays(diff);
  }
  
  public boolean overlaps(GoodDateRange other) {
    return this.start.before(other.end) && this.end.after(other.start);
  }
}

2.2.2 封装的层次

封装可以在不同层次实现:

java
public class WellEncapsulatedClass {
  
  // 层次1:字段级封装
  private final List<Order> orders; // 私有字段
  
  // 层次2:方法级封装
  public List<Order> getOrders() {
    return Collections.unmodifiableList(orders); // 返回不可变视图
  }
  
  // 层次3:类级封装
  // 整个类的实现细节对外部不可见
  private OrderRepository repository;
  private CacheService cache;
  private NotificationService notifier;
  
  // 外部只知道 OrderService 能做什么
  // 不知道它如何做到
  public OrderResult processOrder(Order order) {
    cache.invalidate(order.getId());
    Order saved = repository.save(order);
    notifier.sendConfirmation(saved);
    return OrderResult.success(saved.getId());
  }
}

2.3 继承

2.3.1 继承的正确使用

继承表示 is-a 关系。使用继承前必须确认:

  • 子类确实是父类的一种:如"储蓄账户"是"银行账户"的一种。
  • 子类可以替换父类:Liskov 替换原则。
  • 继承层次不要太深:通常不超过3层。
java
// 正确的继承:is-a 关系成立
public abstract class Vehicle {
  public abstract void start();
  public abstract void stop();
  public abstract int getSpeed();
}

public class Car extends Vehicle {
  @Override
  public void start() { /* 汽车启动 */ }
  @Override
  public void stop() { /* 汽车停止 */ }
  @Override
  public int getSpeed() { /* 返回汽车速度 */ }
}

// Car 确实是 Vehicle 的一种,继承合理

// 错误的继承:is-a 关系不成立
public class Stack extends ArrayList {
  // Stack 不是 ArrayList 的一种
  // 只是"用了" ArrayList
  // 应该使用组合而非继承
}

// 正确的做法:使用组合
public class BetterStack {
  private final List<Object> elements = new ArrayList<>();
  
  public void push(Object element) {
    elements.add(element);
  }
  
  public Object pop() {
    if (elements.isEmpty()) {
      throw new EmptyStackException();
    }
    return elements.remove(elements.size() - 1);
  }
  
  public boolean isEmpty() {
    return elements.isEmpty();
  }
}

2.3.2 继承的替代方案:组合

McConnell 强调:优先使用组合而非继承。组合比继承更灵活,耦合更低。

java
// 使用组合替代继承
// 差:继承
public class FlyingBird extends Bird {
  public void fly() { /* 飞行 */ }
}
public class SwimmingBird extends Bird {
  public void swim() { /* 游泳 */ }
}
// 问题:既能飞又能游泳的鸟怎么办?多重继承?

// 好:组合
public class Bird {
  private final MovementAbility movement;
  
  public Bird(MovementAbility movement) {
    this.movement = movement;
  }
  
  public void move() {
    movement.execute();
  }
}

// 可以灵活组合
public class FlyingAbility implements MovementAbility {
  @Override
  public void execute() { /* 飞行 */ }
}

public class SwimmingAbility implements MovementAbility {
  @Override
  public void execute() { /* 游泳 */ }
}

public class FlyingAndSwimmingAbility implements MovementAbility {
  private final FlyingAbility flying = new FlyingAbility();
  private final SwimmingAbility swimming = new SwimmingAbility();
  
  @Override
  public void execute() {
    flying.execute();
    swimming.execute();
  }
}

2.4 类的接口设计

2.4.1 接口设计原则

好的类接口应该:

  • 简洁:尽量少的方法,每个方法参数尽量少。
  • 一致:命名规范统一,行为模式一致。
  • 易用:让正确的事情容易做,错误的事情难做。
  • 完整:提供所有必要的功能。
java
// 好的接口设计
public class StringBuilder {
  // 简洁:方法数量适中
  public StringBuilder append(String str) { ... }
  public StringBuilder append(int num) { ... }
  public StringBuilder insert(int offset, String str) { ... }
  public StringBuilder delete(int start, int end) { ... }
  public StringBuilder reverse() { ... }
  public String toString() { ... }
  
  // 一致:返回 this 支持链式调用
  // 易用:append 方法接受多种类型
  // 完整:覆盖了字符串构建的所有基本操作
}

// 链式调用让使用更简洁
String result = new StringBuilder()
    .append("Hello")
    .append(" ")
    .append("World")
    .toString();

2.4.2 避免接口污染

java
// 差:接口污染
public class BadInterface {
  // 暴露内部实现
  public void setInternalState(State state) { ... }
  
  // 方法参数过多
  public void createOrder(String customerName, String customerEmail,
                          String productName, int quantity, double price,
                          String shippingAddress, String billingAddress) { ... }
  
  // 返回类型暴露内部
  public HashMap<String, Object> getOrderData() { ... }
}

// 好:清晰的接口
public class GoodInterface {
  // 不暴露内部实现
  // 使用对象封装参数
  public OrderResult createOrder(CreateOrderRequest request) { ... }
  
  // 返回抽象类型
  public Map<String, Object> getOrderData(OrderId id) { ... }
}

// 使用 Builder 模式处理多参数
public class CreateOrderRequest {
  private final String customerName;
  private final String customerEmail;
  private final List<OrderItem> items;
  private final Address shippingAddress;
  
  private CreateOrderRequest(Builder builder) {
    this.customerName = builder.customerName;
    this.customerEmail = builder.customerEmail;
    this.items = builder.items;
    this.shippingAddress = builder.shippingAddress;
  }
  
  public static Builder builder() {
    return new Builder();
  }
  
  public static class Builder {
    private String customerName;
    private String customerEmail;
    private List<OrderItem> items = new ArrayList<>();
    private Address shippingAddress;
    
    public Builder customerName(String name) {
      this.customerName = name;
      return this;
    }
    
    public Builder customerEmail(String email) {
      this.customerEmail = email;
      return this;
    }
    
    public Builder addItem(OrderItem item) {
      this.items.add(item);
      return this;
    }
    
    public Builder shippingAddress(Address address) {
      this.shippingAddress = address;
      return this;
    }
    
    public CreateOrderRequest build() {
      return new CreateOrderRequest(this);
    }
  }
}

2.5 类的内聚性

2.5.1 单一职责原则

一个类应该只有一个变更的理由。如果一个类有多个变更理由,说明它承担了过多职责。

java
// 差:多个职责
public class User {
  // 职责1:用户数据
  private String name;
  private String email;
  
  // 职责2:用户验证
  public boolean isValid() { ... }
  public boolean isEmailValid() { ... }
  
  // 职责3:用户持久化
  public void save() { ... }
  public void delete() { ... }
  
  // 职责4:用户通知
  public void sendNotification(String message) { ... }
}

// 好:单一职责
public class User {
  // 只负责用户数据
  private final String name;
  private final String email;
  
  public String getName() { return name; }
  public String getEmail() { return email; }
}

public class UserValidator {
  // 只负责验证
  public boolean isValid(User user) { ... }
  public boolean isEmailValid(String email) { ... }
}

public class UserRepository {
  // 只负责持久化
  public void save(User user) { ... }
  public void delete(UserId id) { ... }
}

public class UserNotifier {
  // 只负责通知
  public void sendNotification(User user, String message) { ... }
}

三、实战案例

3.1 案例一:设计一个完善的"订单"类

java
// 完整的类设计示例

public class Order {
  // 封装:私有不可变字段
  private final OrderId id;
  private final CustomerId customerId;
  private final List<OrderItem> items;
  private final Address shippingAddress;
  private final LocalDateTime createdAt;
  
  // 可变状态受保护
  private OrderStatus status;
  private Money totalAmount;
  
  // 构造时验证
  public Order(OrderId id, CustomerId customerId, 
               List<OrderItem> items, Address shippingAddress) {
    this.id = Objects.requireNonNull(id, "Order ID is required");
    this.customerId = Objects.requireNonNull(customerId, "Customer is required");
    
    if (items == null || items.isEmpty()) {
      throw new IllegalArgumentException("Order must have at least one item");
    }
    this.items = List.copyOf(items); // 防御性复制
    
    this.shippingAddress = Objects.requireNonNull(shippingAddress);
    this.createdAt = LocalDateTime.now();
    this.status = OrderStatus.CREATED;
    
    recalculateTotal();
  }
  
  // 只读访问
  public OrderId getId() { return id; }
  public CustomerId getCustomerId() { return customerId; }
  public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
  public OrderStatus getStatus() { return status; }
  public Money getTotalAmount() { return totalAmount; }
  
  // 暴露行为
  public boolean isCancellable() {
    return status == OrderStatus.CREATED || status == OrderStatus.CONFIRMED;
  }
  
  public void confirm() {
    if (status != OrderStatus.CREATED) {
      throw new IllegalStateException(
          "Cannot confirm order in status: " + status);
    }
    this.status = OrderStatus.CONFIRMED;
  }
  
  public void cancel() {
    if (!isCancellable()) {
      throw new IllegalStateException(
          "Cannot cancel order in status: " + status);
    }
    this.status = OrderStatus.CANCELLED;
  }
  
  public void addItem(OrderItem item) {
    if (status != OrderStatus.CREATED) {
      throw new IllegalStateException("Cannot add items after confirmation");
    }
    this.items.add(item);
    recalculateTotal();
  }
  
  // 内部实现隐藏
  private void recalculateTotal() {
    this.totalAmount = items.stream()
        .map(item -> item.getPrice().multiply(item.getQuantity()))
        .reduce(Money.ZERO, Money::add);
  }
  
  // 值对象语义
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Order)) return false;
    Order order = (Order) o;
    return id.equals(order.id);
  }
  
  @Override
  public int hashCode() {
    return id.hashCode();
  }
  
  @Override
  public String toString() {
    return "Order{id=" + id + ", status=" + status + 
           ", total=" + totalAmount + "}";
  }
}

四、本章小结

第6章深入讲解了类的设计原则和实践,核心要点包括:

抽象是核心:提取本质特征,忽略无关细节。

封装保护完整性:隐藏实现细节,暴露稳定接口。

继承要谨慎:确认 is-a 关系成立,优先使用组合。

接口要简洁一致:让正确的事情容易做,错误的事情难做。

单一职责是目标:一个类只有一个变更理由。

简单优先:避免过度设计,不要为假想的未来做设计。

McConnell 的核心观点是:好的类设计不是天赋,而是可以学习的原则和实践。掌握这些原则,就能设计出高质量的类。


五、思考与练习

思考题

在你编写的类中,哪些抽象得好?哪些抽象不足或过度抽象?

封装在实际项目中遇到了哪些挑战?如何平衡封装和便利性?

你见过哪些继承滥用的案例?如何用组合替代?

如何判断一个类的接口是否设计得好?

编程练习

设计一个"图书馆管理系统"的类结构,包括 Book、Library、Member 等类。

找到一个"上帝类",将其拆分为多个单一职责的类。

使用 Builder 模式重构一个参数过多的构造函数。

6/12可以工作
0%