《软件设计的哲学》——9. 在一起更好还是分开更好?

64 阅读19分钟

从复杂性封装到结构设计:设计决策的根本问题

第8章我们学会了"封装复杂性"的智慧——当复杂性不可避免时,由1个人承担比让N个人都承担更有效率。但这引出了一个更基础的问题:

给定两个功能,它们应该在同一个地方实现,还是分开实现?

想象你正在设计一个在线购物系统的购物车模块。你发现需要两个功能:

  • 计算总价:包括商品价格、税费、折扣等
  • 验证库存:检查商品是否有足够库存

现在你面临选择:

  • 选择A:创建一个ShoppingCartService同时处理两个功能
  • 选择B:创建PriceCalculatorInventoryValidator两个分离的服务

这个决定看似简单,实际上体现了软件设计中的根本哲学问题:结构决策应该如何做?

核心原则:基于复杂性的结构决策

决策的根本目标

目标只有一个:降低整个系统的复杂性并改善模块化。

很多人本能地认为:"组件越小越好,分得越细越好。"这种想法有一定道理,但忽略了一个重要事实:

分离行为本身也会带来复杂性!

就像拆解一部汽车,如果你把每个螺丝都单独管理,你可能会发现找到正确的螺丝比组装汽车本身更困难。

分离的四种隐性代价

当我们决定分离两个功能时,会付出这些代价:

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); // 光标通常在选择末尾
    }
}

问题分析:

  1. 需要复杂的协调代码:每个操作都要同步两个对象
  2. 状态一致性困难:容易出现光标和选择不匹配的情况
  3. 代码重复:多个地方都有类似的协调逻辑

设计选择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();
        }
    }
}

为什么合并更好?

用相关性的四个信号分析:

  1. ✅ 信息共享:都涉及文本中的位置信息
  2. ✅ 使用模式:几乎每个文本操作都需要同时考虑光标和选择
  3. ✅ 概念重叠:都属于"文本位置管理"
  4. ✅ 理解依赖:很难独立理解光标行为而不考虑选择状态

结果对比:

维度分离设计合并设计
类的数量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;
    }
}

问题:

  1. 受众不同:通用代码面向所有用户,专用代码面向特定场景
  2. 变化原因不同:通用功能因为算法改进而变化,专用功能因为业务需求而变化
  3. 复用性降低:其他项目想用通用功能,但不需要邮件处理逻辑

正确的分离方式

// ✅ 分离通用和专用代码
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) { }
}

优势:

  1. 职责清晰:TextProcessor专注通用功能,EmailTextProcessor专注邮件特定功能
  2. 复用性好:其他模块可以只使用TextProcessor
  3. 维护性高:变化原因不同的代码不会相互影响
  4. 测试简单:可以独立测试通用功能

方法的分割与合并:核心原则在方法层面的应用

前面我们讨论了系统、模块、类层面的合并与分离决策。现在让我们看看第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);
        }
    }
}

适用条件:

  1. 原方法确实做了两件不相关的事情(违反相关性判断)
  2. 有些用户只需要其中一个功能(使用模式不是双向的)
  3. 分解后的方法比原方法更简单
  4. 大多数用户不需要调用所有分解后的方法

模式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();
    }
}

合并的判断标准:

  1. 信息共享:多个小方法操作相同的数据
  2. 使用模式:总是被一起调用,很少独立使用
  3. 概念重叠:都属于同一个高层概念
  4. 理解依赖:很难独立理解其中一个方法的目的

危险信号:联合方法

红旗警告:如果你无法独立理解一个方法,必须同时看另一个方法才能理解,这就是联合方法。

// ❌ 联合方法的例子:违反了独立理解原则
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怎么办?

问题分析:

  1. 违反信息隐藏:暴露了不必要的中间状态
  2. 违反概念完整性:每个方法都不能独立完成一个完整的概念
  3. 增加使用复杂性:调用者必须了解调用顺序
  4. 脆弱的接口:方法间有隐含的依赖关系

✅ 正确的设计:遵循相关性判断

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章在整个设计哲学中的作用:

  1. 深化第8章:从"封装复杂性"进展到"结构复杂性"
  2. 具体化第4章:为创建深模块提供了结构指导
  3. 扩展第5章:信息隐藏的结构应用
  4. 衔接第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. 决策原则:基于复杂性最小化,而不是"分离总是对的"
  2. 分离代价:数量、管理、分离、重复四种隐性成本
  3. 相关性判断:信息共享、使用模式、概念重叠、理解依赖四个信号
  4. 特殊原则:通用代码与专用代码应该分离
  5. 方法层面:相同原则在方法设计中的具体应用
  6. 整体思维:从系统角度优化复杂性分配

实践智慧

记住这个核心智慧: 拆分或合并模块的决定应基于复杂性。选择能够最好地隐藏信息、产生最少依赖关系和最深接口的结构。

设计师的品格:

  • 系统思维:考虑整体复杂性,而不是局部优化
  • 平衡智慧:权衡分离的收益与代价
  • 用户视角:优先考虑使用者的简单性
  • 演化意识:考虑长期的维护和扩展

与学习之旅的衔接

我们已经走过了软件设计的重要里程碑:

  • 第1-3章:建立设计思维和战略视角
  • 第4-6章:掌握创建深模块的技术
  • 第7-8章:学会复杂性的分层和分配
  • 第9章:具备结构决策的智慧

下一步:第10章将学习如何通过更好的错误定义来进一步降低复杂性,这是设计艺术的另一个重要方面。


作者寄语:优秀的软件设计不是机械地应用规则,而是深刻理解复杂性的本质,然后做出明智的权衡。第9章教会你的不仅是技术,更是一种设计哲学:在简单与复杂、合并与分离之间找到最佳平衡点。这需要经验,需要练习,更需要对复杂性的敬畏之心。