从复杂性封装到结构设计:设计决策的根本问题
第8章我们学会了"封装复杂性"的智慧——当复杂性不可避免时,由1个人承担比让N个人都承担更有效率。但这引出了一个更基础的问题:
给定两个功能,它们应该在同一个地方实现,还是分开实现?
想象你正在设计一个在线购物系统的购物车模块。你发现需要两个功能:
- 计算总价:包括商品价格、税费、折扣等
- 验证库存:检查商品是否有足够库存
现在你面临选择:
- 选择A:创建一个
ShoppingCartService同时处理两个功能 - 选择B:创建
PriceCalculator和InventoryValidator两个分离的服务
这个决定看似简单,实际上体现了软件设计中的根本哲学问题:结构决策应该如何做?
核心原则:基于复杂性的结构决策
决策的根本目标
目标只有一个:降低整个系统的复杂性并改善模块化。
很多人本能地认为:"组件越小越好,分得越细越好。"这种想法有一定道理,但忽略了一个重要事实:
分离行为本身也会带来复杂性!
就像拆解一部汽车,如果你把每个螺丝都单独管理,你可能会发现找到正确的螺丝比组装汽车本身更困难。
分离的四种隐性代价
当我们决定分离两个功能时,会付出这些代价:
1. 数量复杂性
// 分离前:管理1个对象
ShoppingCartService cart = new ShoppingCartService();
cart.addItem(item);
result = cart.checkout();
// 分离后:管理3个对象
PriceCalculator priceCalc = new PriceCalculator();
InventoryValidator inventoryValidator = new InventoryValidator();
CheckoutCoordinator coordinator = new CheckoutCoordinator();
// 必须协调它们的工作
if (inventoryValidator.isAvailable(items)) {
Price total = priceCalc.calculate(items);
result = coordinator.process(total, items);
}
问题:组件越多,追踪和管理越困难。
2. 管理复杂性
// 需要额外的"胶水代码"来协调不同组件
public class CheckoutService {
private PriceCalculator priceCalc;
private InventoryValidator inventory;
private PaymentProcessor payment;
public CheckoutResult checkout(Cart cart) {
// 协调3个组件,处理它们之间的状态传递
if (!inventory.reserve(cart.getItems())) {
return CheckoutResult.failure("库存不足");
}
Price total = priceCalc.calculate(cart.getItems());
PaymentResult payResult = payment.process(total);
if (!payResult.isSuccess()) {
inventory.release(cart.getItems()); // 回滚库存
return CheckoutResult.failure("支付失败");
}
return CheckoutResult.success();
}
}
问题:需要编写额外代码来管理组件间的协调。
3. 分离复杂性
// 原本在一个文件中的相关逻辑,现在散布在多个地方
src/services/PriceCalculator.java // 价格计算逻辑
src/services/InventoryValidator.java // 库存验证逻辑
src/services/DiscountEngine.java // 折扣计算逻辑
src/coordinators/CheckoutCoordinator.java // 协调逻辑
// 当你需要理解完整的结账流程时,必须跳转多个文件
问题:相关信息分散,理解和维护变得困难。
4. 重复复杂性
// 每个组件都需要自己的错误处理、日志记录等
public class PriceCalculator {
private static final Logger logger = LoggerFactory.getLogger(PriceCalculator.class);
public Price calculate(List<Item> items) {
try {
logger.info("开始计算价格,商品数量: {}", items.size());
// 价格计算逻辑
} catch (Exception e) {
logger.error("价格计算失败", e);
throw new PriceCalculationException(e);
}
}
}
public class InventoryValidator {
private static final Logger logger = LoggerFactory.getLogger(InventoryValidator.class);
public boolean validate(List<Item> items) {
try {
logger.info("开始验证库存,商品数量: {}", items.size());
// 库存验证逻辑
} catch (Exception e) {
logger.error("库存验证失败", e);
throw new InventoryValidationException(e);
}
}
}
问题:相似的基础设施代码在多个组件中重复。
设计哲学的转变
这要求我们从分离偏见转向整体思维:
分离偏见:
- "小组件一定比大组件好"
- "职责单一就是好设计"
- "分离总是降低复杂性"
整体思维:
- "最小化整个系统的复杂性"
- "权衡分离的收益与代价"
- "考虑组件间的相互作用"
黄金法则:相关性判断
核心原则:将代码放在一起最有益的条件是它们紧密相关。如果无关,则最好分开。
如何判断两个功能是否相关?看这四个信号:
信号1:信息共享
两个功能是否依赖相同的核心信息?
// ✅ 强相关:都依赖文本位置信息
public class TextEditor {
private int cursorPosition;
private int selectionStart;
private int selectionEnd;
private String content;
// 插入文本:需要知道光标位置
public void insertText(String text) {
// 如果有选择,替换选择内容
if (hasSelection()) {
replaceSelection(text);
} else {
insertAtCursor(text);
}
}
// 删除选择:需要知道选择范围
public void deleteSelection() {
if (hasSelection()) {
content = content.substring(0, selectionStart) +
content.substring(selectionEnd);
cursorPosition = selectionStart;
clearSelection();
}
}
}
// ❌ 弱相关:依赖不同的信息
public class MixedService {
public void calculateTax(Order order) {
// 依赖:商品价格、税率表、用户地址
}
public void sendEmail(String email, String content) {
// 依赖:SMTP配置、邮件模板、用户偏好
// 与税务计算没有信息重叠
}
}
信号2:使用模式(必须是双向的!)
使用其中一个功能的人是否很可能也使用另一个?
// ✅ 双向强关联:HTTP请求与响应处理
public class HttpClient {
public Response get(String url) {
// 发送请求的人总是需要处理响应
return processResponse(sendRequest("GET", url, null));
}
private Response sendRequest(String method, String url, String body) { }
private Response processResponse(RawResponse raw) { }
}
// ❌ 单向关联(不适合合并)
// 文件压缩服务几乎总是用哈希算法来验证完整性
// 但哈希算法有很多其他用途(密码、缓存key等)
// 所以不应该合并
public class FileCompressor {
private HashCalculator hashCalc; // 组合,而不是合并
public CompressedFile compress(File file) {
byte[] compressed = doCompress(file);
String hash = hashCalc.calculate(compressed);
return new CompressedFile(compressed, hash);
}
}
信号3:概念重叠
是否存在一个简单的高层概念可以涵盖两个功能?
// ✅ 概念重叠:都属于"用户身份管理"
public class UserIdentityManager {
public boolean authenticate(String username, String password) {
// 验证用户身份
}
public void updatePassword(String username, String newPassword) {
// 更新认证信息
}
public UserProfile getProfile(String username) {
// 获取用户信息
}
}
// ❌ 概念不重叠:强行放在一起
public class MixedUserService {
public boolean authenticate(String username, String password) {
// 用户认证
}
public void sendPromotionalEmail(String username) {
// 营销邮件 - 这属于营销系统,不是身份管理
}
}
信号4:理解依赖
是否很难独立理解其中一个功能而不考虑另一个?
// ✅ 理解依赖:很难单独理解其中一个
public class TransactionManager {
public void begin() {
// 开始事务:必须与commit/rollback一起理解
}
public void commit() {
// 提交事务:必须知道事务的生命周期
}
public void rollback() {
// 回滚事务:与上述功能不可分割
}
}
// ❌ 无理解依赖:可以独立理解
public class UtilityMethods {
public String formatDate(Date date) {
// 日期格式化:完全独立的功能
}
public double calculateDistance(Point a, Point b) {
// 距离计算:与日期格式化无关
}
}
深度案例分析:文本编辑器的光标与选择
让我们通过一个具体例子来理解相关性判断:
设计背景
在GUI文本编辑器中,有两个重要概念:
- 插入光标:闪烁的竖线,指示新字符插入位置
- 选择区域:用户选中的文本范围,通常高亮显示
设计选择A:分离实现
public class InsertionCursor {
private int position;
public void setPosition(int pos) { this.position = pos; }
public int getPosition() { return position; }
public void moveLeft() { position--; }
public void moveRight() { position++; }
}
public class TextSelection {
private int startPos;
private int endPos;
public void setRange(int start, int end) {
this.startPos = start;
this.endPos = end;
}
public boolean hasSelection() { return startPos != endPos; }
public void clear() { startPos = endPos = 0; }
}
public class TextEditor {
private InsertionCursor cursor;
private TextSelection selection;
// 用户按键时的复杂协调逻辑
public void handleKeyPress(char c) {
if (selection.hasSelection()) {
// 删除选中内容
deleteSelectedText();
// 光标移到删除位置
cursor.setPosition(selection.getStartPos());
selection.clear();
}
// 在光标位置插入字符
insertCharacter(c, cursor.getPosition());
cursor.moveRight();
}
// 处理选择操作时的协调
public void selectText(int start, int end) {
selection.setRange(start, end);
cursor.setPosition(end); // 光标通常在选择末尾
}
}
问题分析:
- 需要复杂的协调代码:每个操作都要同步两个对象
- 状态一致性困难:容易出现光标和选择不匹配的情况
- 代码重复:多个地方都有类似的协调逻辑
设计选择B:合并实现
public class TextPosition {
private int cursorPos;
private int selectionStart;
private int selectionEnd;
public void setCursor(int pos) {
this.cursorPos = pos;
// 移动光标时清除选择
this.selectionStart = pos;
this.selectionEnd = pos;
}
public void setSelection(int start, int end) {
this.selectionStart = start;
this.selectionEnd = end;
this.cursorPos = end; // 光标在选择末尾
}
public boolean hasSelection() {
return selectionStart != selectionEnd;
}
public void insertText(String text) {
if (hasSelection()) {
// 替换选中内容
replaceSelection(text);
} else {
// 在光标位置插入
insertAtCursor(text);
}
// 自动更新光标位置
cursorPos += text.length();
}
public void deleteSelection() {
if (hasSelection()) {
deleteRange(selectionStart, selectionEnd);
cursorPos = selectionStart;
clearSelection();
}
}
}
为什么合并更好?
用相关性的四个信号分析:
- ✅ 信息共享:都涉及文本中的位置信息
- ✅ 使用模式:几乎每个文本操作都需要同时考虑光标和选择
- ✅ 概念重叠:都属于"文本位置管理"
- ✅ 理解依赖:很难独立理解光标行为而不考虑选择状态
结果对比:
| 维度 | 分离设计 | 合并设计 |
|---|---|---|
| 类的数量 | 3个 | 1个 |
| 协调代码 | 复杂 | 简单 |
| 状态一致性 | 困难 | 自动保证 |
| API 复杂性 | 高 | 低 |
| 错误概率 | 高 | 低 |
特殊原则:分离通用代码与专用代码
虽然我们强调相关功能应该合并,但有一个重要例外:
通用代码与专用代码应该分离,即使它们看起来相关。
为什么要分离?
// ❌ 混合通用和专用代码
public class MixedTextProcessor {
public String processText(String text, boolean isEmailContent) {
// 通用的文本处理
String cleaned = removeExtraSpaces(text);
String normalized = normalizeLineEndings(cleaned);
// 专用的邮件处理逻辑
if (isEmailContent) {
normalized = addEmailSignature(normalized);
normalized = formatForEmailClient(normalized);
normalized = addTrackingPixel(normalized);
}
return normalized;
}
}
问题:
- 受众不同:通用代码面向所有用户,专用代码面向特定场景
- 变化原因不同:通用功能因为算法改进而变化,专用功能因为业务需求而变化
- 复用性降低:其他项目想用通用功能,但不需要邮件处理逻辑
正确的分离方式
// ✅ 分离通用和专用代码
public class TextProcessor {
// 通用的文本处理:可以被任何项目复用
public String processText(String text) {
String cleaned = removeExtraSpaces(text);
return normalizeLineEndings(cleaned);
}
// 通用的清理方法
private String removeExtraSpaces(String text) { }
private String normalizeLineEndings(String text) { }
}
public class EmailTextProcessor {
private final TextProcessor baseProcessor = new TextProcessor();
// 专用的邮件处理:组合而不是继承
public String processEmailContent(String content) {
String basicProcessed = baseProcessor.processText(content);
String withSignature = addEmailSignature(basicProcessed);
String formatted = formatForEmailClient(withSignature);
return addTrackingPixel(formatted);
}
// 邮件专用的方法
private String addEmailSignature(String text) { }
private String formatForEmailClient(String text) { }
private String addTrackingPixel(String text) { }
}
优势:
- 职责清晰:TextProcessor专注通用功能,EmailTextProcessor专注邮件特定功能
- 复用性好:其他模块可以只使用TextProcessor
- 维护性高:变化原因不同的代码不会相互影响
- 测试简单:可以独立测试通用功能
方法的分割与合并:核心原则在方法层面的应用
前面我们讨论了系统、模块、类层面的合并与分离决策。现在让我们看看第9章的核心原则如何应用到方法层面。
方法层面的决策遵循相同的相关性判断原则:当方法内的不同功能满足四个相关性信号时,应该合并;当它们无关时,应该分离。
分解方法的三种模式
模式1:子任务提取(推荐)
原理:将相关的子任务分解为独立的方法,父方法协调子方法完成整体工作。
// ✅ 子任务提取的正确例子
public class OrderProcessor {
public OrderResult processOrder(Order order) {
// 父方法:协调所有步骤,体现完整的业务流程
ValidationResult validation = validateOrder(order);
if (!validation.isValid()) {
return OrderResult.invalid(validation.getErrors());
}
PaymentResult payment = processPayment(order);
if (!payment.isSuccessful()) {
return OrderResult.paymentFailed(payment.getError());
}
updateInventory(order);
sendConfirmation(order);
return OrderResult.success(order.getId());
}
// 子方法:可以独立理解和测试,具有通用性
private ValidationResult validateOrder(Order order) {
// 订单验证逻辑:可能被其他方法复用
return performValidation(order);
}
private PaymentResult processPayment(Order order) {
// 支付处理逻辑:独立的支付子系统
return paymentGateway.process(order.getPaymentInfo());
}
}
成功的标志:
- 子方法可以独立理解,不需要了解父方法的上下文
- 父方法的逻辑清晰,专注于协调而不是实现细节
- 子方法具有通用性,可能被其他方法复用
- 符合相关性判断:子任务与整体任务在概念上重叠
模式2:平行分解(谨慎使用)
原理:将一个方法分解为多个平级的方法,每个方法对调用者都可见。
// ✅ 合理的平行分解:原方法做了两件不相关的事
public class DocumentConverter {
// 原始方法违反了相关性判断:验证和转换是不同的关注点
public ConversionResult convertDocumentWithValidation(Document doc, Format target) {
// 做了两件事:验证(关注正确性) + 转换(关注格式)
}
// 分解为两个独立的功能,各自关注不同方面
public ValidationResult validateDocument(Document doc) {
// 只做验证:很多用户只需要验证,不需要转换
return performValidation(doc);
}
public ConversionResult convertDocument(Document doc, Format target) {
// 只做转换:假设已经验证过的文档
return performConversion(doc, target);
}
}
// 使用时更加灵活
public class DocumentService {
public void processDocument(Document doc, Format target) {
// 某些情况下只需要验证
if (needsValidationOnly()) {
ValidationResult result = converter.validateDocument(doc);
handleValidationResult(result);
return;
}
// 某些情况下需要完整流程
ValidationResult validation = converter.validateDocument(doc);
if (validation.isValid()) {
ConversionResult conversion = converter.convertDocument(doc, target);
handleConversion(conversion);
}
}
}
适用条件:
- 原方法确实做了两件不相关的事情(违反相关性判断)
- 有些用户只需要其中一个功能(使用模式不是双向的)
- 分解后的方法比原方法更简单
- 大多数用户不需要调用所有分解后的方法
模式3:功能合并(消除浅方法)
原理:当多个小方法违反相关性判断、造成不必要的分离时,将它们合并。
// ❌ 过度分离:违反了相关性判断的浅方法
public class OverSeparatedCalculator {
public double getPrice(Product product) {
return product.getBasePrice(); // 浅方法,没有提供价值
}
public double getTax(Product product, Region region) {
return getPrice(product) * region.getTaxRate(); // 强依赖getPrice
}
public double getShipping(Product product, Address address) {
return calculateShippingCost(product.getWeight(), address);
}
public double getTotal(Product product, Region region, Address address) {
// 必须调用所有上述方法,违反了独立性
return getPrice(product) + getTax(product, region) + getShipping(product, address);
}
}
// ✅ 合理合并:满足相关性判断的深方法
public class OrderCalculator {
public OrderTotal calculateTotal(Product product, Region region, Address address) {
// 所有计算都与"订单总价"这个概念相关
double basePrice = product.getBasePrice();
double tax = basePrice * region.getTaxRate();
double shipping = calculateShippingCost(product.getWeight(), address);
return new OrderTotal(basePrice, tax, shipping);
}
// 如果确实需要单独的计算,提供专门的方法
public double calculateTaxOnly(Product product, Region region) {
return product.getBasePrice() * region.getTaxRate();
}
}
合并的判断标准:
- 信息共享:多个小方法操作相同的数据
- 使用模式:总是被一起调用,很少独立使用
- 概念重叠:都属于同一个高层概念
- 理解依赖:很难独立理解其中一个方法的目的
危险信号:联合方法
红旗警告:如果你无法独立理解一个方法,必须同时看另一个方法才能理解,这就是联合方法。
// ❌ 联合方法的例子:违反了独立理解原则
public class BadUserService {
public void prepareUser(String userId) {
// 第一阶段:做了一些不完整的工作
// 但这个状态对外部不可见,也不完整
userCache.put(userId, loadPartialUserData(userId));
// 调用者不知道"准备"了什么,这个方法没有完整的意义
}
public User completeUser(String userId) {
// 第二阶段:完成用户数据构建
// 但必须先调用prepareUser,否则这个方法无法理解
PartialUserData partial = userCache.get(userId);
return buildCompleteUser(partial);
// 这个方法单独看也没有意义
}
}
// 使用时的困惑:违反了接口简洁性
service.prepareUser("123"); // 为什么要先准备?准备了什么?
User user = service.completeUser("123"); // 为什么要完成?如果忘记prepare怎么办?
问题分析:
- 违反信息隐藏:暴露了不必要的中间状态
- 违反概念完整性:每个方法都不能独立完成一个完整的概念
- 增加使用复杂性:调用者必须了解调用顺序
- 脆弱的接口:方法间有隐含的依赖关系
✅ 正确的设计:遵循相关性判断
public class UserService {
public User getUser(String userId) {
// 一个方法完成一个完整的概念:获取用户
return buildCompleteUser(userId);
}
private PartialUserData loadPartialUserData(String userId) {
// 私有的实现细节,符合信息隐藏
}
private User buildCompleteUser(String userId) {
// 内部使用 loadPartialUserData,但外部不感知
// 这些私有方法满足相关性判断:都为了完成"获取用户"这个目标
PartialUserData partial = loadPartialUserData(userId);
return enhanceUserData(partial);
}
}
方法设计的相关性判断
在方法层面应用相关性判断的具体标准:
1. 信息共享判断
// ✅ 强相关:操作相同的核心数据
public void updateUserProfile(String userId, ProfileData newData) {
User user = loadUser(userId); // 操作用户数据
validateProfileData(newData, user); // 操作相同的用户数据
user.updateProfile(newData); // 操作相同的用户数据
saveUser(user); // 操作相同的用户数据
// 所有步骤都围绕同一个用户对象
}
// ❌ 弱相关:操作不同的数据
public void mixedOperation(String userId, String productId) {
updateUserLastLogin(userId); // 操作用户数据
updateProductInventory(productId); // 操作商品数据
sendMarketingEmail(); // 操作邮件系统
// 这些操作没有共同的数据,应该分离
}
2. 使用模式判断
// ✅ 总是一起使用的功能
public void processPayment(PaymentRequest request) {
validatePaymentInfo(request); // 处理支付总是需要验证
chargeCard(request); // 验证后总是需要扣款
updateOrderStatus(request); // 扣款后总是需要更新状态
sendConfirmation(request); // 更新后总是需要发送确认
}
// ❌ 很少一起使用的功能
public void userManagementMixed(String userId) {
updateUserProfile(userId); // 用户资料更新:经常单独使用
deleteUserAccount(userId); // 删除账户:很少与更新同时使用
generateUserReport(userId); // 生成报告:完全独立的需求
}
3. 概念重叠判断
// ✅ 同一概念的不同方面
public void establishDatabaseConnection(String url, Credentials creds) {
validateConnectionParams(url, creds); // 都属于"建立连接"
createPhysicalConnection(url); // 都属于"建立连接"
authenticateConnection(creds); // 都属于"建立连接"
configureConnectionSettings(); // 都属于"建立连接"
}
// ❌ 不同概念强行组合
public void mixedDataOperations(String data) {
validateDataFormat(data); // 属于"数据验证"概念
encryptData(data); // 属于"数据安全"概念
sendDataViaEmail(data); // 属于"数据传输"概念
backupDataToCloud(data); // 属于"数据备份"概念
}
4. 理解依赖判断
// ✅ 必须一起理解的功能
public void processTransaction(Transaction txn) {
beginTransaction(txn); // 很难独立理解:开始什么?
executeOperations(txn); // 很难独立理解:在什么上下文中执行?
commitTransaction(txn); // 很难独立理解:提交什么?
// 这些操作必须作为一个整体来理解"事务处理"
}
// ❌ 可以独立理解的功能
public void independentUtilities(String input) {
String formatted = formatText(input); // 可以独立理解:格式化文本
Date parsed = parseDate(input); // 可以独立理解:解析日期
boolean valid = validateEmail(input); // 可以独立理解:验证邮箱
// 这些功能各自独立,没有理解依赖
}
与设计哲学体系的关系
在复杂性管理体系中的地位
第9章在整个设计哲学中的作用:
- 深化第8章:从"封装复杂性"进展到"结构复杂性"
- 具体化第4章:为创建深模块提供了结构指导
- 扩展第5章:信息隐藏的结构应用
- 衔接第10章:为错误定义和处理的结构设计铺垫
与前面原则的协同
// 综合应用前面所有原则的例子
public class SmartEmailService {
// 第4章:深模块 - 简单接口,强大功能
public EmailResult sendEmail(String to, String subject, String content) {
return sendEmail(to, subject, content, EmailOptions.standard());
}
// 第5章:信息隐藏 - 隐藏SMTP、模板、重试等复杂性
// 第8章:封装复杂性 - 内部处理各种错误情况
// 第9章:合并相关功能 - 邮件发送的所有方面统一管理
public EmailResult sendEmail(String to, String subject, String content, EmailOptions options) {
try {
EmailMessage message = buildMessage(to, subject, content, options);
DeliveryResult result = deliverWithRetry(message, options.getRetryPolicy());
logDelivery(message, result);
return EmailResult.fromDelivery(result);
} catch (Exception e) {
return EmailResult.failure(translateError(e));
}
}
// 分离的功能:通用的模板处理(可被其他系统复用)
private final TemplateEngine templateEngine = new TemplateEngine();
// 合并的功能:邮件构建、发送、重试、日志记录
private EmailMessage buildMessage(String to, String subject, String content, EmailOptions options) {
// 内部协调多个相关步骤
}
private DeliveryResult deliverWithRetry(EmailMessage message, RetryPolicy policy) {
// 发送和重试逻辑紧密相关,合并处理
}
}
实践决策框架
决策流程图
遇到两个功能需要决定是否合并
↓
检查相关性信号
┌─────────┼─────────┐
↓ ↓ ↓
信息共享? 使用模式? 概念重叠? 理解依赖?
↓ ↓ ↓ ↓
YES YES YES YES
└─────────┼─────────┘
↓
多个信号都是YES?
↓
YES → 检查是否为通用vs专用代码
↓
是 → 分离
否 → 合并
↓
NO → 分离
实践检查清单
设计时的检查清单:
合并的信号(多个YES表示应该合并):
- 两个功能是否共享核心信息?
- 使用其中一个功能的人是否总是使用另一个?(双向)
- 是否存在简单的概念可以涵盖两个功能?
- 是否很难独立理解其中一个功能?
- 合并后的接口是否比分离的接口更简单?
分离的信号(任一YES表示应该分离):
- 两个功能是否有不同的变化原因?
- 其中一个是通用的,另一个是专用的?
- 合并后是否违反了单一职责原则?
- 合并后的类是否变得过于复杂?
- 有很多用户只需要其中一个功能?
方法设计时的检查清单:
- 子方法是否可以独立理解?
- 避免了联合方法的问题吗?
- 分解是否真的简化了调用者的使用?
- 父方法的接口是否保持不变?(子任务提取)
- 分解后的方法是否比原方法更简单?(平行分解)
结论与最终思考
核心要点总结
- 决策原则:基于复杂性最小化,而不是"分离总是对的"
- 分离代价:数量、管理、分离、重复四种隐性成本
- 相关性判断:信息共享、使用模式、概念重叠、理解依赖四个信号
- 特殊原则:通用代码与专用代码应该分离
- 方法层面:相同原则在方法设计中的具体应用
- 整体思维:从系统角度优化复杂性分配
实践智慧
记住这个核心智慧: 拆分或合并模块的决定应基于复杂性。选择能够最好地隐藏信息、产生最少依赖关系和最深接口的结构。
设计师的品格:
- 系统思维:考虑整体复杂性,而不是局部优化
- 平衡智慧:权衡分离的收益与代价
- 用户视角:优先考虑使用者的简单性
- 演化意识:考虑长期的维护和扩展
与学习之旅的衔接
我们已经走过了软件设计的重要里程碑:
- 第1-3章:建立设计思维和战略视角
- 第4-6章:掌握创建深模块的技术
- 第7-8章:学会复杂性的分层和分配
- 第9章:具备结构决策的智慧
下一步:第10章将学习如何通过更好的错误定义来进一步降低复杂性,这是设计艺术的另一个重要方面。
作者寄语:优秀的软件设计不是机械地应用规则,而是深刻理解复杂性的本质,然后做出明智的权衡。第9章教会你的不仅是技术,更是一种设计哲学:在简单与复杂、合并与分离之间找到最佳平衡点。这需要经验,需要练习,更需要对复杂性的敬畏之心。