第5章 软件构建中的设计
一、导读
1.1 本章学习目标
第5章讲解了软件设计在构建阶段的应用。McConnell 强调,设计不是一次性的前期活动,而是贯穿整个构建过程的持续活动。本章的核心学习目标包括:
- 理解设计的层次:架构设计、详细设计和编码设计各自关注什么。
- 掌握核心设计原则:模块化、信息隐藏、高内聚低耦合。
- 学会在编码中应用设计原则:设计不是纸上谈兵,而是每天的编码实践。
- 理解设计模式的价值:复用经过验证的设计方案。
- 掌握设计改善的方法:重构是持续改善设计的关键技术。
- 建立"设计是持续活动"的认知:设计不是一次性的,而是与编码并行的。
1.2 为什么本章如此重要
很多程序员认为"设计是架构师的事",自己只需要按照设计文档编码。这种想法是极其错误的。McConnell 指出:每个程序员每天都在做设计决策——类的划分、方法的组织、变量的命名、逻辑的安排,这些都是设计。
设计质量直接决定了代码的可维护性。糟糕的设计导致代码难以理解、难以修改、难以测试;优秀的设计让代码清晰、灵活、可扩展。
本章的重要性在于它将设计从"高大上"的概念拉回到日常编码实践中。你会理解设计不是遥不可及的理论,而是每天编码时都在做的事情。掌握设计原则,就是掌握了写出高质量代码的核心能力。
1.3 核心问题引导
为什么说"每个程序员都是设计师"?你在编码时做了哪些设计决策?
模块化的本质是什么?如何判断一个模块是否设计良好?
信息隐藏为什么如此重要?如何在实际编码中实现信息隐藏?
高内聚和低耦合如何同时实现?它们之间有什么权衡?
设计模式是"银弹"吗?如何避免过度使用设计模式?
二、核心概念详解
2.1 设计的层次
2.1.1 架构设计
架构设计关注系统的整体结构:
- 组件划分:系统由哪些主要组件组成?
- 组件关系:组件之间如何交互?
- 技术选型:使用什么技术栈?
- 关键决策:性能、安全、扩展性等关键问题的解决方案。
// 架构设计示例:电商系统
// 组件划分
public class ECommerceArchitecture {
// 前端层
private final WebFrontend webFrontend;
private final MobileApp mobileApp;
// API网关层
private final APIGateway gateway;
// 业务服务层
private final OrderService orderService;
private final ProductService productService;
private final UserService userService;
private final PaymentService paymentService;
// 数据层
private final OrderRepository orderRepo;
private final ProductRepository productRepo;
private final UserRepository userRepo;
// 基础设施层
private final MessageQueue messageQueue;
private final CacheService cache;
}2.1.2 详细设计
详细设计关注每个组件的内部结构:
- 类设计:类的职责、接口、关系。
- 数据结构:数据模型、存储结构。
- 算法设计:关键算法的选择和设计。
- 接口设计:API 的输入输出规格。
// 详细设计示例:订单服务
public class OrderServiceDesign {
// 类设计
// OrderService: 协调订单流程
// OrderValidator: 验证订单数据
// OrderPricer: 计算订单价格
// OrderRepository: 持久化订单
// 接口设计
public interface OrderService {
OrderResult createOrder(CreateOrderRequest request);
OrderResult updateOrder(UpdateOrderRequest request);
OrderResult cancelOrder(CancelOrderRequest request);
Order getOrder(OrderId id);
}
// 数据结构设计
public class Order {
private final OrderId id;
private final CustomerId customerId;
private final List<OrderItem> items;
private final Money totalAmount;
private OrderStatus status;
private final Address shippingAddress;
private final LocalDateTime createdAt;
}
}2.1.3 编码设计
编码设计关注每个函数、每个语句的具体实现:
- 函数设计:参数、返回值、内部逻辑。
- 语句组织:条件判断、循环、异常处理。
- 变量使用:命名、作用域、生命周期。
// 编码设计示例:计算订单折扣
public class OrderDiscountCalculator {
// 函数设计:清晰的输入输出
public DiscountResult calculateDiscount(Order order) {
// 防御式编程
Objects.requireNonNull(order);
// 策略选择
DiscountStrategy strategy = selectStrategy(order);
// 计算折扣
Money discount = strategy.apply(order);
// 验证结果
validateDiscount(discount, order.getTotalAmount());
return new DiscountResult(discount, strategy.getName());
}
// 语句组织:清晰的逻辑流
private DiscountStrategy selectStrategy(Order order) {
if (order.hasCoupon()) {
return new CouponStrategy(order.getCoupon());
}
if (order.getCustomer().isVip()) {
return new VipStrategy();
}
if (order.getTotalAmount().exceeds(Money.of(500))) {
return new ThresholdStrategy();
}
return new NoDiscountStrategy();
}
}2.2 核心设计原则
2.2.1 模块化
模块化的本质是将复杂系统分解为可管理的部分。每个模块应该:
- 有明确的职责:每个模块只做一件事。
- 有清晰的接口:模块之间通过定义良好的接口交互。
- 有独立的实现:模块的内部实现对外部不可见。
// 良好的模块化
// 每个类有明确的单一职责
public class OrderProcessor {
private final OrderValidator validator;
private final PriceCalculator priceCalculator;
private final InventoryManager inventoryManager;
private final PaymentProcessor paymentProcessor;
private final NotificationService notificationService;
public OrderResult process(Order order) {
// 每个步骤委托给专门的模块
validator.validate(order);
Money total = priceCalculator.calculate(order);
inventoryManager.reserve(order.getItems());
try {
paymentProcessor.charge(order.getCustomer(), total);
notificationService.sendConfirmation(order);
return OrderResult.success(order.getId());
} catch (PaymentException e) {
inventoryManager.release(order.getItems());
return OrderResult.paymentFailed(e.getMessage());
}
}
}
// 每个模块内部实现独立
public class PriceCalculator {
private final DiscountEngine discountEngine;
private final TaxCalculator taxCalculator;
private final ShippingCalculator shippingCalculator;
public Money calculate(Order order) {
Money subtotal = calculateSubtotal(order);
Money discount = discountEngine.apply(order);
Money tax = taxCalculator.calculate(subtotal.minus(discount));
Money shipping = shippingCalculator.calculate(order);
return subtotal.minus(discount).plus(tax).plus(shipping);
}
}2.2.2 信息隐藏
信息隐藏(Information Hiding)是 David Parnas 提出的核心设计原则。它的核心思想是:每个模块应该隐藏其内部实现细节,只暴露必要的接口。
信息隐藏的好处:
- 降低耦合:模块之间不依赖内部实现。
- 提高可维护性:修改内部实现不影响其他模块。
- 支持并行开发:不同团队可以独立开发不同模块。
// 信息隐藏的示例
// 差:暴露内部实现
public class BadOrder {
// 直接暴露内部数据结构
public List<OrderItem> items;
public double totalAmount;
public String status;
}
// 好:隐藏内部实现
public class GoodOrder {
// 内部数据不可直接访问
private final List<OrderItem> items;
private Money totalAmount;
private OrderStatus status;
// 通过方法暴露行为,而非数据
public Money getTotalAmount() {
return totalAmount; // 返回不可变对象
}
public boolean isCancellable() {
return status == OrderStatus.CREATED
|| status == OrderStatus.CONFIRMED;
}
public void confirm() {
if (!isCancellable()) {
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);
}
}2.2.3 高内聚
高内聚(High Cohesion)意味着模块内部的元素紧密相关。内聚性有多种类型:
- 功能内聚(最好):模块内所有元素共同完成一个功能。
- 顺序内聚:一个元素的输出是另一个元素的输入。
- 通信内聚:元素操作相同的数据。
- 时间内聚(较差):元素在同一时间段执行。
- 逻辑内聚(差):元素在逻辑上相似。
- 巧合内聚(最差):元素之间没有关系。
// 功能内聚:所有方法围绕"订单处理"这一功能
public class OrderProcessor {
public OrderResult process(Order order) { ... }
private void validate(Order order) { ... }
private Money calculateTotal(Order order) { ... }
private void reserveInventory(Order order) { ... }
private void chargePayment(Order order, Money amount) { ... }
private void sendConfirmation(Order order) { ... }
}
// 巧合内聚(差):方法之间没有关系
public class UtilityClass {
public void processOrder() { ... }
public void formatReport() { ... }
public void sendEmail() { ... }
public void backupDatabase() { ... }
}2.2.4 低耦合
低耦合(Low Coupling)意味着模块之间的依赖关系尽量少。耦合的类型:
- 无直接耦合(最好):模块之间没有直接关系。
- 数据耦合:通过参数传递数据。
- 印记耦合:传递复杂数据结构的一部分。
- 控制耦合:传递控制信息(如标志位)。
- 公共耦合:共享全局数据。
- 内容耦合(最差):直接访问另一个模块的内部数据。
// 数据耦合(好):通过参数传递简单数据
public class GoodDesign {
public Money calculateTax(Money amount, TaxRate rate) {
return amount.multiply(rate.getValue());
}
}
// 内容耦合(差):直接访问内部数据
public class BadDesign {
public void processOrder(Order order) {
// 直接修改 order 的内部状态
order.internalData.status = "PROCESSED";
order.internalData.processedDate = new Date();
}
}
// 使用接口降低耦合
public interface PaymentGateway {
PaymentResult charge(CustomerId customer, Money amount);
}
// 依赖接口而非具体实现
public class PaymentService {
private final PaymentGateway gateway; // 依赖抽象
public PaymentService(PaymentGateway gateway) {
this.gateway = gateway;
}
}2.3 设计模式的应用
2.3.1 何时使用设计模式
McConnell 强调,设计模式不是"银弹",不应该为了使用而使用。使用设计模式的正确时机是:
- 问题匹配:当前问题确实符合模式的适用场景。
- 复杂度合理:模式引入的复杂度在可接受范围内。
- 团队理解:团队成员理解模式的含义和代价。
// 策略模式:当有多种算法可以互换时使用
public interface PricingStrategy {
Money calculatePrice(Order order);
}
public class StandardPricing implements PricingStrategy {
@Override
public Money calculatePrice(Order order) {
return order.getSubtotal();
}
}
public class WholesalePricing implements PricingStrategy {
@Override
public Money calculatePrice(Order order) {
return order.getSubtotal().multiply(0.85);
}
}
// 观察者模式:当需要通知多个对象时使用
public interface OrderEventListener {
void onOrderCreated(Order order);
void onOrderCancelled(Order order);
}
public class OrderService {
private final List<OrderEventListener> listeners = new ArrayList<>();
public void addListener(OrderEventListener listener) {
listeners.add(listener);
}
public OrderResult createOrder(CreateOrderRequest request) {
Order order = buildOrder(request);
// 通知所有监听者
listeners.forEach(l -> l.onOrderCreated(order));
return OrderResult.success(order.getId());
}
}2.3.2 避免过度设计
过度设计比不设计更糟糕。McConnell 建议:
- YAGNI(You Aren't Gonna Need It):不要为假想的未来需求做设计。
- KISS(Keep It Simple, Stupid):保持简单。
- 三次法则:第一次直接实现,第二次复制,第三次再抽象。
// 过度设计:为假想的未来需求做准备
public class OverDesignedPaymentService {
// 假设未来可能需要支持100种支付方式
private final Map<String, PaymentStrategy> strategies;
private final PaymentStrategyFactory factory;
private final PaymentStrategyRegistry registry;
private final PaymentStrategyValidator validator;
// ...大量抽象层,但当前只需要支持信用卡
public OverDesignedPaymentService() {
// 复杂的初始化逻辑
}
}
// 简单设计:只解决当前问题
public class SimplePaymentService {
public PaymentResult chargeCreditCard(CreditCard card, Money amount) {
// 直接实现,清晰简洁
PaymentGateway gateway = new StripeGateway();
return gateway.charge(card, amount);
}
}三、实战案例
3.1 案例一:从混乱到清晰的设计改善
// 改善前:混乱的设计
public class GodClass {
// 一个类做了所有事
public void processOrder(Order order) {
// 验证
if (order.getItems().isEmpty()) throw new RuntimeException();
if (order.getCustomer() == null) throw new RuntimeException();
// 计算价格
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
if (order.hasCoupon()) total *= 0.9;
total *= 1.08; // 税
// 保存
database.save(order);
// 发邮件
Email email = new Email();
email.setTo(order.getCustomer().getEmail());
email.setSubject("Order Confirmation");
email.send();
}
}
// 改善后:清晰的设计
public class OrderProcessor {
private final OrderValidator validator;
private final PriceCalculator priceCalculator;
private final OrderRepository repository;
private final NotificationService notifier;
public OrderProcessor(OrderValidator validator,
PriceCalculator priceCalculator,
OrderRepository repository,
NotificationService notifier) {
this.validator = validator;
this.priceCalculator = priceCalculator;
this.repository = repository;
this.notifier = notifier;
}
public OrderResult process(Order order) {
validator.validate(order);
Money total = priceCalculator.calculate(order);
order.setTotal(total);
repository.save(order);
notifier.sendConfirmation(order);
return OrderResult.success(order.getId());
}
}
// 每个模块职责清晰
public class OrderValidator {
public void validate(Order order) {
if (order.getItems().isEmpty()) {
throw new OrderValidationException("Order must have items");
}
if (order.getCustomer() == null) {
throw new OrderValidationException("Order must have customer");
}
}
}
public class PriceCalculator {
private static final TaxRate DEFAULT_TAX = TaxRate.of(0.08);
public Money calculate(Order order) {
Money subtotal = calculateSubtotal(order);
Money discount = applyDiscount(order, subtotal);
Money tax = calculateTax(subtotal.minus(discount));
return subtotal.minus(discount).plus(tax);
}
private Money calculateSubtotal(Order order) {
return order.getItems().stream()
.map(item -> item.getPrice().multiply(item.getQuantity()))
.reduce(Money.ZERO, Money::add);
}
private Money applyDiscount(Order order, Money subtotal) {
if (order.hasCoupon()) {
return subtotal.multiply(0.1);
}
return Money.ZERO;
}
private Money calculateTax(Money amount) {
return amount.multiply(DEFAULT_TAX);
}
}四、本章小结
第5章讲解了软件设计在构建阶段的应用,核心要点包括:
设计有三个层次:架构设计、详细设计和编码设计,每个层次都很重要。
模块化是基础:将复杂系统分解为可管理的部分。
信息隐藏是关键:隐藏实现细节,暴露稳定接口。
高内聚低耦合是目标:让模块专注单一职责,减少模块间依赖。
设计模式是工具:在合适的时候使用,不要过度设计。
设计是持续活动:与编码并行,通过重构持续改善。
McConnell 的核心观点是:设计不是架构师的专利,而是每个程序员的日常实践。掌握设计原则,就是掌握了写出高质量代码的核心能力。
五、思考与练习
思考题
在你编写的代码中,哪些类/模块设计得好?哪些需要改善?为什么?
信息隐藏在实际项目中遇到了哪些挑战?如何克服?
你见过哪些过度设计的案例?如何避免?
设计模式在你的项目中是如何使用的?有没有滥用?
编程练习
找到一段"上帝类"代码,将其拆分为多个高内聚的类。
为一个"图书馆管理系统"设计类结构,体现模块化和信息隐藏原则。
使用策略模式重构一个包含大量 if-else 的代码。