软件设计的哲学: 第六章 更深的通用模块

在设计新模块时,最多见的一个决定就是以通用方式仍是特殊方式实现它。有些人可能会说,应该采用通用的方法,即实现一种机制,用于解决普遍的问题,而不只仅是当前重要的问题。在这种状况下,新机制可能会在将来发现意想不到的用途,从而节省时间。通用方法彷佛与第3章中讨论的投资心态一致,即您预先花费更多的时间来节省之后的时间。编辑器

另外一方面,咱们知道很难预测软件系统的将来需求,因此一个通用的解决方案可能包含一些实际上并不须要的设施。此外,若是您实现了一些过于通用的东西,那么它可能没法很好地解决您如今面临的特定问题。所以,有些人可能会说,最好关注今天的需求,只构建您知道本身须要的东西,并根据您今天计划使用它的方式进行专门化。若是采用特殊用途的方法,并在之后发现其余用途,则始终能够对其进行重构,使其成为通用用途。专用方法彷佛与软件开发的增量方法一致。函数

6.1 使类具备必定的通用性

根据个人经验,最好的方法是以某种通用的方式实现新模块。短语“某种程度上通用的”意思是模块的功能应该反映您当前的需求,可是它的接口不该该。相反,接口应该足够通用,以支持多种用途。该接口应该易于使用,以知足今天的须要,而不是专门针对他们。“有些”这个词很重要:不要得意忘形,不要构建一些通用的东西,由于它很难知足您当前的需求。工具

通用方法最重要的(可能也是最使人惊讶的)好处是,它比专用方法产生更简单、更深刻的接口。若是您将该类用于其余目的,那么通用方法还能够在未来为您节省时间。然而,即便该模块仅用于其原始目的,因为其简单性,通用目的的方法仍然更好。学习

6.2 示例:为编辑器存储文本

让咱们考虑一个来自软件设计课程的例子,在这个课程中,学生被要求构建简单的GUI文本编辑器。编辑器必须显示一个文件,并容许用户指向、单击和键入来编辑文件。编辑器必须在不一样的窗口中支持同一文件的多个同步视图;他们还必须支持多级撤销和重作文件的修改。spa

每一个学生项目都包含一个管理文件底层文本的类。文本类一般提供将文件加载到内存、读取和修改文件文本以及将修改后的文本写回文件的方法。设计

许多学生团队为text类实现了特殊用途的api。他们知道这个类将在交互式编辑器中使用,因此他们考虑了编辑器必须提供的特性,并根据这些特定的特性定制了文本类的API。例如,若是编辑器的用户键入退格键,编辑器将当即删除光标左边的字符;若是用户键入删除键,编辑器将当即删除光标右侧的字符。了解了这一点,一些团队在text类中建立了一个方法来支持这些特定的特性:code

void backspace(Cursor cursor);
void delete(Cursor cursor);

这些方法中的每个都以光标的位置做为参数;特殊类型游标表示此位置。编辑器还必须支持能够复制或删除的选择。学生们经过定义一个选择类,并在删除期间将这个类的对象传递给text类来处理这个问题:对象

void deleteSelection(Selection selection);

学生们可能认为若是text类的方法对应于用户可见的特性,那么实现用户界面会更容易。然而,在现实中,这种专门化对用户界面代码几乎没有什么好处,并且它为用户界面或文本类的开发人员带来了很高的认知负荷。text类以大量的浅层方法结束,每一个浅层方法只适合一个用户界面操做。许多方法(如delete)只在一个地方调用。所以,开发用户界面的开发人员必须了解文本类的大量方法。接口

这种方法在用户界面和文本类之间形成了信息泄漏。与用户界面相关的抽象,如选择或退格键,反映在文本类中;这增长了开发人员处理文本类的认知负荷。每个新的用户界面操做都须要在text类中定义一个新方法,所以处理用户界面的开发人员可能最终也要处理text类。类设计的目标之一是容许独立地开发每一个类,可是专门化的方法将用户界面和文本类绑定在一块儿。

6.3更通用的API

更好的方法是使text类更通用。它的API应该只根据基本的文本特性来定义,而不反映将用它实现的高级操做。例如,只须要两个方法来修改文本:

oid insert(Position position, String newText);
void delete(Position start, Position end);

第一个方法在文本中的任意位置插入任意字符串,第二个方法删除大于或等于开始但小于结束位置的全部字符。这个API还使用了一个更通用的类型Position而不是游标,它反映了一个特定的用户界面。text类还应该提供一些通用的工具来处理文本中的位置,例如:

Position changePosition(Position position, int numChars);

此方法返回一个新位置,该位置距离给定位置有必定数量的字符。若是numChars参数为正,则新位置在文件中的时间晚于位置;若是数字是负数,则新位置在位置以前。该方法在必要时自动跳转到下一行或上一行。使用这些方法,能够用如下代码实现delete键(假设游标变量保存当前游标位置):

text.delete(cursor, text.changePosition(cursor, 1));

一样,backspace键能够实现以下:

text.delete(text.changePosition(cursor, -1), cursor);

使用通用的文本API,实现用户界面功能(如删除和退格)的代码比使用专门的文本API的原始方法要长一些。然而,新代码比旧代码更明显。在用户界面模块中工做的开发人员可能关心backspace键删除哪些字符。对于新代码,这是显而易见的。使用旧的代码,开发人员必须转到text类并阅读backspace方法的文档和/或代码来验证行为。此外,与专门化方法相比,通用方法整体上的代码更少,由于它用更少的通用方法替换了文本类中大量的专用方法。

使用通用接口实现的文本类能够用于交互编辑器以外的其余用途。例如,假设您正在构建一个应用程序,该应用程序经过用另外一个字符串替换全部特定字符串的出现来修改指定的文件。专门化文本类(如backspace和delete)中的方法对这个应用程序没有什么价值。可是,通用文本类已经具有了新应用程序所需的大部分功能。惟一缺乏的是一个方法来搜索下一个出现的给定字符串,如这个:

Position findNext(Position start, String string);

固然,交互式文本编辑器可能具备搜索和替换的机制,在这种状况下,text类已经包含此方法。

6.4 通用性使得信息隐藏效果更好

通用方法在文本和用户接口类之间提供了更清晰的分离,从而实现更好的信息隐藏。文本类不须要知道用户界面的细节,好比如何处理退格键;这些细节如今封装在user interface类中。能够添加新的用户界面特性,而无需在text类中建立新的支持函数。通用接口还减小了认知负担:开发人员只需学习一些简单的方法,这些方法能够用于各类目的。

text类的原始版本中的backspace方法是一个错误的抽象。它的目的是隐藏关于删除哪些字符的信息,但用户界面模块确实须要知道这一点;用户界面开发人员可能会阅读backspace方法的代码以确认它的准确行为。将这个方法放到text类中只会让用户界面开发人员更难得到他们须要的信息。软件设计最重要的元素之一是决定谁须要知道什么,何时须要知道。当细节很重要时,最好让它们尽量明确和明显,好比backspace操做的修改实现。将这些信息隐藏在接口后面只会形成不透明性。

6.5 问本身的问题

识别干净的通用类设计要比建立一个类容易。下面是一些您能够问本身的问题,这些问题将帮助您在接口的通常用途和特殊用途之间找到适当的平衡。

什么是最简单的接口能够知足我当前的全部需求?若是您减小了API中的方法数量,而没有减小它的总体功能,那么您可能正在建立更通用的方法。 特殊用途的文本API至少有三种删除文本的方法:backspace、delete和deleteSelection。更通用的API只有一个用于删除文本的方法,这知足了全部三个目的。只有在每一个方法的API保持简单的状况下,减小方法的数量才有意义;若是为了减小方法的数量,您不得不引入许多额外的参数,那么您可能并无真正地简化事情。

在多少状况下会使用这种方法?若是一个方法是为一个特定的用途而设计的,好比backspace方法,那么这就是一个危险信号,由于它可能太特殊了。看看是否能够将几个专用方法替换为一个通用方法。

这个API容易用于我当前的需求吗?这个问题能够帮助您肯定何时您在使API变得简单和通用方面作得太过火了。若是您必须编写大量额外的代码来使用一个类来知足当前的须要,那么接口没有提供正确的功能就是一个危险的信号。例如,text类的一种方法是围绕单字符操做进行设计:insert插入单个字符,delete删除单个字符。这个API既简单又通用。可是,对于文本编辑器来讲,它并不特别容易使用:高级代码将包含许多循环来插入或删除字符范围。对于大型操做,单字符方法的效率也很低。所以,文本类最好内置对字符范围的操做的支持。

6.6 结论

与专用接口相比,通用接口有许多优势。它们每每更简单,包含更少的方法。它们还提供了类之间更清晰的分离,而特殊用途的接口每每会泄漏类之间的信息。使您的模块具备必定的通用功能是下降整个系统复杂性的最佳方法之一。

相关文章
相关标签/搜索