第6章 可以工作的类
一、导读
1.1 本章学习目标
第6章深入讲解了面向对象编程中类的设计原则和实践。McConnell 详细介绍了如何创建高质量的类。本章的核心学习目标包括:
- 掌握抽象的本质:提取本质特征,忽略无关细节。
- 理解封装的重要性:隐藏实现细节,保护数据完整性。
- 学会正确使用继承:is-a 关系的判断,避免继承滥用。
- 掌握类的接口设计:简洁、一致、易用。
- 理解类的内聚性:单一职责原则的应用。
- 避免过度设计:简单优先,不要为假想的未来做设计。
1.2 为什么本章如此重要
类是面向对象编程的基本构建块。类的设计质量直接决定了整个系统的可维护性、可扩展性和可复用性。McConnell 指出:好的类设计让代码易于理解、易于修改、易于测试;糟糕的类设计让代码难以理解、难以修改、难以测试。
本章的重要性在于它提供了类设计的系统化方法。不是凭直觉设计类,而是基于明确的原则和实践。你会理解为什么有些类设计得好,有些设计得差,以及如何改善类的设计。
1.3 核心问题引导
什么是抽象?如何在类设计中正确地抽象?
封装的目的是什么?如何在实际编码中实现封装?
继承是"银弹"吗?什么时候应该使用继承,什么时候应该使用组合?
类的接口应该如何设计?如何做到简洁又完整?
如何判断一个类的职责是否过多?如何拆分?
二、核心概念详解
2.1 抽象
2.1.1 抽象的本质
抽象是提取事物的本质特征,忽略无关细节的过程。在类设计中,抽象意味着:
- 提取核心概念:识别问题域中的关键实体。
- 定义核心行为:确定实体应该做什么。
- 隐藏实现细节:不关心具体怎么做。
// 抽象的示例:银行账户
// 核心概念:银行账户
// 核心行为:存款、取款、查询余额
// 隐藏细节:如何持久化、如何计算利息、如何处理并发
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 抽象的层次
抽象不是一次性的,而是有层次的:
// 层次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 封装的目的
封装的目的不仅是"隐藏数据",更是:
- 保护数据完整性:防止外部代码随意修改数据。
- 隐藏实现细节:内部实现可以随意修改。
- 提供清晰的接口:让使用者只关心"做什么"而非"怎么做"。
// 封装的示例:日期范围
// 差:暴露内部数据
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 封装的层次
封装可以在不同层次实现:
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层。
// 正确的继承: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 强调:优先使用组合而非继承。组合比继承更灵活,耦合更低。
// 使用组合替代继承
// 差:继承
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 接口设计原则
好的类接口应该:
- 简洁:尽量少的方法,每个方法参数尽量少。
- 一致:命名规范统一,行为模式一致。
- 易用:让正确的事情容易做,错误的事情难做。
- 完整:提供所有必要的功能。
// 好的接口设计
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 避免接口污染
// 差:接口污染
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 单一职责原则
一个类应该只有一个变更的理由。如果一个类有多个变更理由,说明它承担了过多职责。
// 差:多个职责
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 案例一:设计一个完善的"订单"类
// 完整的类设计示例
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 模式重构一个参数过多的构造函数。