《软件设计的哲学》——6. 通用模块更深入

89 阅读10分钟

深模块设计的下一个问题

前面两章我们学习了创建深模块的核心技术:

  • 第4章:理解深模块的价值(简单接口+强大实现)
  • 第5章:掌握信息隐藏的技巧(隐藏复杂性)

现在我们面临一个更具体的设计挑战:当我们设计一个新模块时,应该让它专门解决当前问题,还是设计得更通用一些?

这个问题看似简单,但实际上触及了软件设计的核心困境。

设计中的永恒困境

设计新模块时,我们面临一个根本性的选择:通用设计 vs 专用设计

现实场景: 你正在开发一个文本编辑器,需要一个管理文本内容的类。你会怎么设计?

选项A:专用设计

// 为编辑器定制的API
class TextEditor {
    void backspace();        // 处理退格键
    void delete();           // 处理删除键
    void insertChar(char c); // 插入字符
    void newLine();          // 处理回车
}

选项B:通用设计

// 通用的文本操作API
class TextBuffer {
    void insert(int position, String text);
    void delete(int start, int length);
    String getText(int start, int length);
    int length();
}

哪个更好?这个选择会影响你的整个设计。

两种设计哲学的对比

通用方式的观点:

  • 实现能解决广泛问题的机制,不仅限于当前重要的问题
  • 新机制可能在未来发现意外用途,节省时间
  • 符合第3章的投资思维:前期多花时间,后期节省时间

专用方式的观点:

  • 很难预测软件系统的未来需求
  • 通用解决方案可能包含永远用不到的功能
  • 过于通用可能无法很好地解决当前的具体问题
  • 专注当前需求,后续需要时再重构为通用
  • 符合增量开发的理念

作者的建议:"Somewhat General-Purpose"

以我的经验,最有效的办法是以某种通用的方式实现新模块

"有点通用"的核心理念

这个概念包含一个重要的区分:

  • 模块的功能应该反映当前需求(不要过度设计)
  • 模块的接口不应该反映当前需求(应该足够通用)

关键原则:

  • 接口应该足够通用以支持多种用途
  • 接口应该易于满足当前需求,而不被具体需求绑架
  • "有点"很重要:不要过度通用,避免为当前需求增加使用难度

通用设计的主要收益

最重要的收益:接口简化

**令人惊讶的发现:**通用方式最重要的(也许是令人惊讶的)好处是,与专用方式相比,它能产生更简单、更深的接口。

1. 更简单、更深的接口

  • 方法数量更少,但每个方法功能更强大
  • 符合第4章"深模块"的理念
  • 比专用方式的接口更简单

2. 更好的信息隐藏

  • 在类之间提供更清晰的分离
  • 专用接口倾向于在类之间泄漏信息
  • 每个模块的职责更加明确

3. 降低认知负担

  • 开发者只需学习少量简单的方法
  • 这些方法可以重复用于各种目的
  • 减少需要记忆的API数量

4. 未来价值(额外收益)

  • 如果将来重用该类用于其他目的,通用方式可以节省时间
  • 但即使模块永远只用于原始目的,通用方式仍然更好

经典案例:文本编辑器

背景:学生项目需求

学生们需要构建简单的GUI文本编辑器,要求:

  • 显示文件并允许用户点击和输入来编辑
  • 支持同一文件在不同窗口的多个同时视图
  • 支持多级撤销和重做功能

每个项目都包含一个管理文件底层文本的类,提供加载文件、读取和修改文本、写回文件等方法。

❌ 专用设计:学生的直觉方法

许多学生团队实现了专用API,他们知道这个类要用于交互式编辑器,所以考虑编辑器必须提供的功能,并将文本类的API定制为这些特定功能。

// 专用设计:每个编辑器功能对应一个方法
class TextClass {
    void backspace();        // 处理退格键:删除光标左边的字符
    void delete();           // 处理删除键:删除光标右边的字符
    void deleteSelection();  // 删除选中的文本
    void insertChar(char c); // 插入单个字符
    void newLine();          // 处理回车键
    // ... 更多专用方法
}

这种设计的问题

  • 方法数量激增:每个UI操作都需要一个新方法
  • 职责混乱:文本存储类为什么要知道"退格键"的概念?
  • 信息泄漏:UI层的概念渗透到了数据存储层
  • 扩展困难:要添加新的编辑功能就必须在文本类中添加新方法

✅ 通用设计:更好的方法

更好的方法是为文本类提供通用的文本操作接口:

// 通用设计:基础操作的组合
class TextClass {
    void insert(int position, String text);
    void delete(int start, int length);
    String getText(int start, int length);
    int length();
}

现在所有的编辑器操作都可以用这几个通用方法来实现:

// UI层实现具体操作
void handleBackspace() {
    if (cursorPosition > 0) {
        textBuffer.delete(cursorPosition - 1, 1);
        cursorPosition--;
    }
}

void handleDelete() {
    if (cursorPosition < textBuffer.length()) {
        textBuffer.delete(cursorPosition, 1);
    }
}

void handleDeleteSelection() {
    textBuffer.delete(selectionStart, selectionEnd - selectionStart);
}

这种设计的优势

  • 接口简化:从多个专用方法减少到4个通用方法
  • 职责清晰:文本类专注于文本操作,不需要了解UI概念
  • 信息隐藏更好:文本类不需要知道退格键如何处理
  • 易于扩展:新的UI功能可以通过组合现有方法实现,无需修改文本类

设计指导:三个关键问题

识别干净的通用类设计比创建一个要容易。以下三个问题可以帮助你在接口的通用性和专用性之间找到正确的平衡:

1. "满足当前所有需求的最简单接口是什么?"

如果你能减少API中的方法数量而不降低其整体功能,那么你可能正在创建更通用的方法。

例子:专用文本API至少有三种删除文本的方法:backspace、delete和deleteSelection。更通用的API只有一种删除文本的方法,可以满足所有三个目的。

注意:只有在每个方法的API保持简单的前提下,减少方法数量才有意义。如果你必须引入大量额外参数来减少方法数量,那么你可能并没有真正简化事情。

2. "这个方法会在多少情况下使用?"

如果一个方法是为特定用途设计的(如backspace方法),这是一个危险信号,表明它可能过于专用。看看是否可以用一个通用方法替换几个专用方法。

3. "这个API对当前需求容易使用吗?"

这个问题可以帮助你确定何时在使API简单和通用方面走得太远了。如果你必须编写大量额外代码才能将类用于当前目的,这是一个危险信号,表明接口没有提供正确的功能。

过度抽象的例子:文本类的一种方法是围绕单字符操作设计:insert插入单个字符,delete删除单个字符。这个API既简单又通用,但对文本编辑器来说不是特别容易使用:高级代码会包含大量循环来插入或删除字符范围,对大型操作也会效率低下。

通用设计中的重要洞察

backspace方法:错误抽象的典型例子

文本类原始版本中的backspace方法是一个错误的抽象。它声称隐藏了关于删除哪些字符的信息,但用户界面模块实际上需要知道这些信息;用户界面开发人员很可能会阅读backspace方法的代码来确认其精确行为。

问题分析

  • 将方法放在文本类中只会让用户界面开发人员更难获得所需信息
  • 这种隐藏在接口后面的信息只会产生模糊性,而非清晰性

设计的核心原则

软件设计最重要的元素之一是确定谁需要知道什么,以及何时知道。当细节重要时,最好让它们明确且尽可能明显,例如修订后的退格操作实现。

通用方法带来的额外好处

通用方法在文本和用户界面类之间提供了更清晰的分离:

  1. 更好的信息隐藏:文本类无需了解用户界面的具体细节,如退格键如何处理
  2. 封装性增强:这些细节现在封装在用户界面类中
  3. 扩展性更好:可以添加新的用户界面功能,而无需在文本类中创建新的支持函数
  4. 认知负担降低:用户界面开发人员只需学习几个简单方法,就可以重复用于各种目的

通用设计的核心价值与实践指导

令人惊讶的发现

通用设计最重要的价值不是复用,而是简化。

即使一个模块永远只用于其原始目的,通用方式仍然更好,因为它带来了:

  1. 更简单的接口:方法更少但功能更强
  2. 更清晰的职责分离:每个模块专注于自己的核心职责
  3. 更好的信息隐藏:减少模块间的信息泄漏

关键要点回顾

  1. "有点通用"的智慧

    • 功能反映当前需求(不过度设计)
    • 接口足够通用(支持灵活使用)
  2. 三个设计问题

    • 满足当前需求的最简单接口是什么?
    • 这个方法会在多少场景下使用?
    • 这个API对当前需求好用吗?
  3. 通用设计的本质

    • 抓住问题的本质,而不是表面现象
    • 让接口反映问题的核心,而不是特定用法

实践指导

设计时的思考流程

  1. 理解问题本质:这个模块要解决的核心问题是什么?
  2. 识别通用操作:有哪些基础操作可以组合出所有需要的功能?
  3. 验证接口设计:用三个关键问题检验设计
  4. 平衡通用性:避免过度抽象,确保当前用法简单

常见错误与避免

  • ❌ 为每个UI操作创建专门方法(过度专用)
  • ❌ 设计过于抽象的接口(过度通用)
  • ✅ 找到问题的本质操作,设计组合性强的接口

与前面章节的关系

知识体系的完善

  • 第4章(深模块):告诉我们要追求简单接口+强大实现
  • 第5章(信息隐藏):告诉我们如何隐藏复杂性
  • 第6章(通用模块):告诉我们如何设计简单而强大的接口

这三章共同构成了深模块设计的完整方法论。

下一步学习

设计技巧的进一步应用

  • 第7章:如何在系统层面应用这些原则(分层设计)
  • 后续章节:具体的设计技巧和实践方法

开始实践

立即可以应用的方法

  • 在设计新模块时问自己三个关键问题
  • 重构现有的专用接口为更通用的设计
  • 寻找可以合并的相似方法
  • 让接口反映问题本质而不是具体用法

核心思想:通用设计不是为了预测未来,而是为了简化现在。好的抽象来自于对当前问题本质的深入理解,而不是对未来需求的猜测。