Saleri

The Essence of Software.Chapter.04.概念结构

最后一次修改于

到目前为止,我一直在用相当模糊和概括的术语来讨论概念。但概念到底什么?要有效地使用概念,我们需要超越一般性并着眼于具体细节。在这一章中,我将向你展示如何构建一个概念的定义。这种结构将阐明概念是什么(以及不是什么);它将为你提供设计概念的路线图;并且它将允许你精确地定义每个概念。

我将使用三个概念作为范例,重点关注它们的结构,但也会讲述关于它们发明和使用的更宏大的故事,并指出它们的一些设计细节。

当然,概念并不能消除所有的设计问题。然而,它们确实通过将挑战识别为特定于某个特定概念的挑战,来帮助你局部化挑战。一个概念不仅成为它所体现的行为的容器,而且还成为所有关于其设计的积累知识、随着其部署在实践中出现的问题,以及设计师处理这些问题的各种方式的容器。

苹果的杀手级概念:垃圾桶 (Trash)

trash (垃圾桶) 概念是苹果公司在1982年为Macintosh的前身Lisa计算机发明的。垃圾桶图标(以及当你把东西放进去时它可爱的鼓起版本,以及你清空它时发出的俏皮的咀嚼声)成为了Macintosh声称其是一个更友好、更可用的操作系统的象征(图4.1)。从那时起,trash概念就变得无处不在,不仅出现在其他操作系统的文件系统管理器中,还出现在许多其他应用程序中。

乍一看,垃圾桶图标似乎只是提供了一个直观的删除文件和文件夹的手势——通过将它们拖到垃圾桶,而不是执行更传统的删除命令。然而,真正的创新不在于你可以将东西移垃圾桶,而在于你可以随后将它们移。垃圾桶保存着一系列项目,可以通过打开它来查看;然后,你可以通过将项目从垃圾桶移动到其他位置来恢复它们。因此,我们可能会说,trash概念的目的 (purpose)不是删除 (deletion),而是删除的撤销 (undoing)

图4.1 原始的Macintosh桌面,右下角有垃圾桶 (1984年)。

当然,你还必须能够永久删除文件,以便为新文件腾出空间。这是通过“清空 (emptying)”垃圾桶来实现的。所以,总而言之,当你想删除一个项目时,你把它移到垃圾桶;当你想恢复它时,你把它从垃圾桶里拿出来;当你空间不足并想永久移除它所包含的项目时,你清空垃圾桶。

为了使概念设计变得实用,我们需要一种简洁而准确地描述概念的方法。图4.2展示了如何描述trash概念。这些部分完全对应于我刚才给你的解释。

首先是概念的名称 (name)。与名称一起的是可以被实例化时特化的类型列表——这里只有一个类型,Item (项目)。因此,在一个实例化中,这些项目可能成为文件系统的文件;在另一个实例化中,它们可能是电子邮件客户端的邮件。

名称之后是概念目的 (purpose)的简练总结。然后是状态 (state),它将构成概念的事物组织成各种结构。在这个例子中,只有两个集合:accessible (可访问的),即在垃圾桶外部可访问的项目集合;以及trashed (已废弃的),即已废弃项目的集合,即那些已被删除但尚未被永久移除的项目。

图4.2 定义的trash (垃圾桶) 概念。

动作 (actions)描述了概念的动态性和行为。动作是瞬间发生的——也就是说,不占用时间——但它们之间可以经过任意长的时间。对动作的描述说明了发生该动作时状态是如何更新的——例如,删除一个项目会将其从可访问项目集合移动到已废弃项目集合。它还可能包含一个限制动作何时可以发生的前提条件 (precondition)——例如,你只能删除一个可访问但尚未废弃的项目。(除了在非正式概述中提到的动作之外,完整的描述还包括一个 create (创建) 动作以保持完整性,因为被删除的项目必须以某种方式产生。)

最后是操作原理 (operational principle),它展示了动作是如何实现目的的,并包含一个或多个核心的使用场景。在这个例子中,有两个场景。一个是关于恢复的:它说在你删除了项目 x 之后,你可以恢复它,然后该项目又是可访问的。另一个是关于永久移除的:它说在你删除一个项目后,你可以清空垃圾桶,这样该项目将既不可访问也不在垃圾桶中。

在狭义的技术层面上,操作原理并没有增加任何东西,因为你可以从动作规范中推断出任何场景。但是,为了理解为什么概念被设计成这样,以及期望如何使用它,操作原理是至关重要的。

所有这些都可以变得更加精确,通过一种行为的数学模型,以及一种用于定义动作和操作原理的形式化符号。具体细节对大多数读者来说并不重要,所以我把它们放在了章末注记中。

垃圾桶概念:设计缺陷终于得到修复

这个 trash 概念非常成功,并被广泛使用。它出现在所有的图形文件管理器(Mac、Windows 和 Linux)、电子邮件客户端(如 Apple Mail 和 Gmail)以及云存储系统(如 Dropbox 和 Google Drive)中。并非所有的概念实例都提供完全相同的行为;在一个常见的变体中,项目在被删除经过一定时间(比如 30 天)后,会从垃圾桶中永久移除。

在 Macintosh 上,整个系统只有一个垃圾桶,这带来了一些不幸的后果。首先,当你插入和移除外部驱动器时,随着属于这些驱动器的已删除项目的来来去去,垃圾桶的内容也会增加和减少。这可能会让人感到轻微的不安:你可能会在垃圾桶中看到一个项目并打算恢复它,但随后发现它已经消失了(因为驱动器已被弹出)。

一个更实质性的问题出现在以下场景中。假设你插入一个 USB 闪存盘(一个小型外部闪存驱动器)并尝试将一个文件复制到上面,但是空间不够。所以你决定通过废弃闪存盘上已有的一些文件来腾出一些空间。你尝试再次将文件复制到闪存盘上,但它再次失败。然后你意识到,废弃这些文件并没有通过永久移除它们来真正腾出空间;要做到这一点,你需要清空垃圾桶。

但现在你进退两难了。如果你不清空垃圾桶,你就无法将文件复制到 USB 闪存盘。但如果你清空了,你将丢失之前从硬盘驱动器中删除的所有文件,从而消除了将来恢复它们的选项。

令人惊讶的是,这个问题在30多年里一直悬而未决,直到 2015 年发布的苹果操作系统 OS X El Capitan 才最终得到解决。这个解决方案也许更像是一个变通方法,它提供了一个“立即删除 (delete immediately)”动作,允许你一键永久移除垃圾桶中选定的项目。

图4.3 Microsoft Word (左) 和 Apple Pages (右) 中的style (样式) 概念。

另一个设计缺陷涉及垃圾桶中文件的列出方式。几十年来,没有办法按删除日期对垃圾桶中的项目进行排序。这意味着如果你不小心删除了一个文件,然后去垃圾桶希望恢复它,你可能会倒霉。如果你明智地让垃圾桶一直增长直到你真正需要空间,你的垃圾桶将包含数千个文件。如果你删除了一个文件且不记得它的名字,你就没办法找到它。

在 2011 年(随着 OS X Lion 的发布),苹果允许你按文件夹中项目的“添加日期”对其进行排序,对于垃圾桶而言,这对应于删除日期。(在第 6 章中,我将更详细地解释这种设计,并展示它如何涉及概念的巧妙融合。)

桌面排版背后的概念:样式 (Style)

我们的第二个例子是 style (样式) 的概念,之前已经提到过,并在图 3.2 的 Adobe InDesign 中以及图 4.3 的 Microsoft Word 和 Apple Pages 中展示过。它的目的是为了更容易实现一致的格式化。

要使用它,你可以将样式分配给文档中的段落。例如,你可以将样式 heading 分配给每个对应于章节标题的段落。现在,如果你想把所有的标题都变成粗体,你只需把 heading 样式的格式设置改为 bold (粗体),所有的标题段落就会同步更新。

这就是操作原理。这实际上是一个相当复杂的场景,它涉及到创建多个段落,将单一的样式分配给其中多个段落,然后修改该样式。操作原理并不总是最简单的场景,但它演示目的如何实现的最细微的场景。显然,为了演示段落的一致格式化,你需要不止一个段落。在概念定义中,操作原理表述为,如果你定义了一个样式 s 使其具有格式 f,将其分配给两个元素 e1e2,然后重新定义 s 使其具有格式 f’,那么 e1e2 都会拥有那个新的格式。

图4.4 定义的style (样式) 概念。

为了让这种魔法奏效,概念状态(见图 4.4)必须相当复杂。有两个映射,一个(我称之为 assigned,已分配)将每个元素与其分配的唯一样式相关联,另一个(我称之为 defined,已定义)将每种样式与其定义的格式相关联。在这个描述中,“格式”是一个抽象的东西,你可以将其视为体现了所有的格式属性(粗体、12pt、Times Roman 等)。作为一种简写,引入了状态的第三个组成部分,称为 format,代表这两个映射的组合(写为 assigned.defined),因此,如果一个元素 e 被分配了样式 s,并且样式 s 被定义为具有格式 f,那么元素 e 就具有格式 f

我定义了两个动作:一个将样式分配给一个元素,另一个为给定的样式定义格式。第二个动作既用于创建一个具有给定格式的样式,也用于用新格式更新一个现有的样式。它们也可以作为单独的动作用列出;这两种方法都是有效的。

图4.5 应用于 Microsoft PowerPoint 幻灯片主题 (左) 和 Adobe 应用程序色板 (右) 的style概念。

样式化的事物:不总是货真价实

style (样式) 概念被广泛使用。它出现在 Microsoft Word 和 Apple Pages 等文字处理器中,以及 Adobe InDesign 和 QuarkXPress 等桌面排版工具中,不仅用于设置段落样式,还用于设置字符范围的样式。它被用于 Microsoft PowerPoint 的“颜色主题 (color themes)”,其中包含了一系列预定义的样式,用于为幻灯片上出现的各种文本(如标题、超链接、正文)和背景着色(图 4.5,左)。Web 格式化语言——层叠样式表 (CSS) 的“类 (classes)”也是样式,它们在这里提供了格式与内容的清晰分离。

有时,样式概念的使用并不是一目了然的。在 Adobe InDesign 和 Adobe Illustrator 中,你可以通过应用颜色色板 (color swatch) 来对元素进行着色(图 4.5,右)。一开始你可能没有注意到的是,这些颜色色板是可修改的。如果你用红色色板给几个元素着色,它们都会变成红色——这不足为奇。但是现在,如果你打开红色色板并调整其滑块将其变成绿色,你会看到所有那些红色的元素现在都变成了绿色。这当然非常有用,因为它让你能够轻松地维护一致的调色板,而不必在开始时就决定调色板的所有颜色。

图4.6 两个看起来像样式但其实不是的概念:Apple拾色器 (左) 和 Apple TextEdit 中的样式 (右)。

其他时候,你会遇到一些看起来像是某个概念实例,但结果却不是的东西。苹果的拾色器在苹果的所有应用程序中用于选择颜色,看起来与 Adobe 的颜色色板非常相似(见图 4.6,左),所以我们可能会猜测它是 style 概念的一个实例。但是如果你尝试一下,你会发现虽然你可以删除一个色板或添加一个新的色板,但你不能改变现有色板的颜色。这种修改样式格式的能力对于 style 概念来说是必不可少的。如果没有它,这个概念根本无法运作——也就是说,它的操作原理会失效。添加一个新样式对与旧样式关联的元素没有影响,所以一旦定义了它们的格式,就无法同步更改元素。

另一个差之毫厘的是苹果的基本文字处理器 TextEdit 的“样式”(图 4.6,右)。这个名字暗示了 style 概念,你确实不仅可以创建和删除命名的“样式”,而且还可以修改它们。然而,当你将样式应用于段落时,它会更新段落的格式化,但是样式并没有附着在上面:样式与段落之间没有持久的关联。因此,更改样式只会影响其未来的使用,而不会影响它之前所应用到的段落。

style 概念已经以多种方式得到了丰富,其中许多涉及格式相互叠加:例如,部分样式 (partial styles),其格式仅设置某些属性(如使文本变为斜体,但不影响其大小);样式继承 (style inheritance),其中一个样式被定义为另一个样式的扩展;以及覆盖 (overrides),其中元素的格式由带有某些覆盖格式的样式定义。

图4.7 定义的reservation (预订) 概念。

19世纪的一个概念:预订 (Reservation)

作为本章的最后一个例子,让我们考虑一个早在软件出现之前就存在的熟悉的概念。reservation (预订) 概念(图 4.7)有助于有效利用有限的资源池。提供者希望资源被尽可能多地使用;消费者希望在需要时资源是可用的。

它是这样运作的。想要使用资源的消费者试图进行预订。如果该资源还没有预订,尝试就会成功。然后,当消费者稍后来使用资源时,资源将对其可用。

要运行这个概念,你需要跟踪一组预订,每个预订都与被保留的资源和预订它的消费者相关联。除了进行预订并最终使用资源外,消费者通常还可以在决定不再需要时取消预订。

当然,这有点在赘述显而易见的事情。你已经在当地餐厅预订过餐桌、在图书馆预订过书、在音乐会预订过座位等等,所以这些都不是新事物。但值得注意的是这种解释的形式。我从目的(有效利用资源)开始;然后我给出了操作原理(如何进行和实现预订);然后是状态(一组预订);最后是动作(reserve 预订,use 使用和 cancel 取消)。

对这个概念的描述使用了一个集合(用来记住哪些资源仍然可用),以及一个针对预订的从用户到资源的映射。与定义 style 概念时的映射不同,这种映射是一对多的:单个用户可以预订多个资源。

动作还包括资源所有者(例如餐厅)执行的动作,用于提供和收回资源。如果资源已被预订,收回资源是很棘手的。为了简单起见,定义中说在这种情况下你不能收回,但在实践中更好的设计会允许这种收回,并隐式取消预订。最后,操作原理补充了一个我非正式叙述中遗漏的注意事项:你只有在进行预订后,没有在使用它之前取消,才能使用该预订。

设计师的顾虑 (Reservations)

像任何其他概念一样,reservation 有许多变体和附加功能。通常资源与一个时间段绑定。在餐厅预订系统中,消费者只选择开始时间,而结束时间是隐式的,由餐厅所有者决定(这是一件棘手的事情,因为把就餐时间设置得太长意味着接待较少的顾客,但设置得太短会导致预订的顾客等待)。资源可能与特定的物理对象相关联(例如飞机上的座位),或者它可能代表某个类别的可替代成员(例如餐厅里的任何一张桌子,或特定书籍的任何副本)。

因为预订通常是免费的,提供者可能需要防范那些预订了资源但从不使用的用户。餐厅预订系统通过添加一个由餐厅所有者执行的 no-show (未出现) 动作来做到这一点;如果顾客有太多次未出现,他们的账户就会被暂停。另一种策略是防止消费者预订不能一起使用的资源,例如在同一晚预订两家不同餐厅的桌子。航空公司有复杂的规则来检测冲突的预订,这会导致奇怪的异常情况。

reservation 概念非常有用,它在许多不同的领域都有应用。在铁路信号系统中,通过要求火车在进入轨道段之前预订它们来实现安全性;这样,系统可以确保没有两列火车同时占据同一段轨道。在网络互连中,有一种称为 RSVP (资源预留协议) 的协议,它允许路由器预留带宽,以便它们在需要时能保证一定水平的网络性能(称为“服务质量”)。

教训与实践 (Lessons & Practices)

本章的一些教训:

  • 概念定义包括其名称、目的、状态、动作和操作原理。操作原理展示了行为如何实现目的,是理解一个概念的关键,它可能不是最简单的场景。
  • 每一个概念都是某个时候某个人为了某种目的发明的。广泛使用的大多数概念随着时间的推移都经历了广泛的开发和完善。
  • 大多数概念是通用的,可以应用于不同上下文中不同种类的数据。通用性提供了重用的可能,并有助于提炼概念的本质。
  • 概念可以独立地设计和理解,通过将设计分解成不同的子问题来简化软件设计,其中许多子问题可以通过重用现有的概念来解决。

以及你现在可以应用的一些实践:

  • 为了设计或分析一个软件产品,从识别概念开始。对于每一个概念,选择一个好名字,找一个关于目的的精辟总结,并制定一个操作原理。为了更深入地探讨,列出所有的动作,并弄清楚需要什么状态来支持它们。
  • 如果你的概念缺乏有趣的行为——你想不出一个引人注目的操作原理,甚至无法列出动作——它可能根本不是一个概念,或者你需要扩展它以识别真正的概念。
  • 拿出一个系统的数据库模式或类结构,将其表示为实体-关系图,然后将其分解为更小的图(在实体上重叠但在关系上不重叠),每一个图体现了某种功能片段。这些更小的图就是不同概念的状态。
  • 反过来,在设计数据模型时,与其将其视为一个单体,不如独立地为你每个概念开发局部的“微模型 (micromodels)”,然后在共同实体上合并它们以形成全局模型。
  • 作为一个有趣且有益的练习,挑选一个有趣的概念并研究它的历史。它可能是一个独立于软件存在的概念(比如 reservation),或者它可能是你最喜欢的应用程序中的一个概念。它是何时被发明的?是谁发明的?随着时间的推移它是如何演变的?