Saleri

The Essence of Software.Chapter.07.概念依赖

最后一次修改于

当概念被组合时,它们在彼此的关系中扮演着特定的角色。例如,当我们将 label (标签)和 todo (待办事项)结合起来,以便用户可以将标签附加到待办任务时,todo 概念就变成了应用 label 概念的一种主体。

组合本身是对称的,因为同步将同步的动作视为平等的。然而,组合可能会在一种概念增强另一种概念功能的方式上引入不对称性。label 概念扩展了 todo 概念的功能:现在,除了添加任务之外,我们还可以为它们打上标签。反过来则是没有意义的:没有人会从构建一个用于给事物打标签的应用开始,然后用可以被标记的任务来扩展它。

这些不对称性揭示了软件产品中重要的结构,本章将对此进行探讨。我将解释概念之间的 依赖 (dependence)概念,它可以用一个简单的图表来描绘,该图表为概念及其角色提供了有用的总结。

在我坚持认为概念是相互独立的情况下,引入一种依赖的概念似乎有些自相矛盾。实际上这里并没有矛盾。概念的最本质特征确实是它们可以被孤立地理解和实现。本章讨论的依赖关系源于概念在产品整体上下文中所扮演的角色,并且更多地是产品的属性,而不是其包含的概念的属性。

逐个概念地发展软件产品

有些软件产品必须“出生即成熟”。飞机或核电站不能部署仅是“最小可行性产品”的软件,并计划在需求出现时调整软件。

但在大多数情况下,增量开发更好,因为它允许开发人员尽早获得对其设计工作的反馈,评估已部署工作的价值,并在发现不匹配之处时进行处理。因此,将设计新的软件产品看作是每次发展几个概念的 成长 (growing)过程是很有用的。

并非所有的增长都涉及添加概念。有时一个概念会被移除,也许是因为人们发现它的用处不如预期,或者因为它存在一些不易补救的严重缺陷,又或者因为它的功能可以通过扩展另一个概念来被涵盖。有时,现有的概念将被提炼和润色,理想情况下,这不仅使它们变得更加强大,而且(在目的和操作原理上)更加令人信服。有时候——也是最令人兴奋的情况——会发现一种协同效应(无论是否添加了概念),它在不伴随复杂性增加的情况下扩展了产品的功能。

不受控制的增长可能成为优秀产品的绊脚石。对于小而成功的系统进行雄心勃勃的重新设计,尤其容易受到具有影响力的 IBM 经理弗雷德·布鲁克斯 (Fred Brooks) 所谓的“第二系统效应” (the second-system effect) 的影响,即过度自信导致臃肿和不必要复杂的解决方案。

由于所有这些原因,能够简洁地表示软件产品可能的增长方式是很有用的,而且——同样重要的是——能够表示它可以被精简的方式。这就是概念依赖图所提供的功能。

构建概念清单

我在第 3 章中提到过,概念如何为你提供一个应用程序的地图——一种概念清单,让你概述其功能和目的。让我们使用一个虚构的应用程序作为插图来看看这张地图是如何产生的。

在整个过程中,我将假设你正在独自设计这个应用程序;当然,在实践中,大多数设计工作都是在团队中完成的。我也将只讨论包含哪些概念,忽略有关概念实际设计的所有关键问题。

假设你喜欢鸟鸣,并想构建一个应用程序来帮助人们通过鸟叫声来识别鸟类。这就是核心需求:有人听到了鸟叫声,想知道这是哪种鸟发出的。而且大概有这种需求的人也会想听听特定种类鸟类的歌声。

你思考这样一个应用可能如何运作。也许一个用户上传了一个音轨,然后其他人听了并给出建议。现在你开始进行头脑风暴,想一些概念。在发明一个新概念之前,你试图寻找一些适合的现成概念。论坛(如 StackExchange、Quora、Piazza 等)中使用的 q&a (问答)概念怎么样?在其中,一个人提出一个问题,而其他人提供答案。

实际上,你可能会问,为什么不直接使用那些现有的应用之一呢?通常那将是解决问题的最佳途径。为了确保这不是最佳途径,你会想注意到现有解决方案的一些局限性,并检查它们是否真的很重要。也许在这种情况下,你发现它们中没有一个能让你轻松地上传和回放录音;也许拥有一个良好的集成是很重要的,这样你就可以通过点击几下录制一首歌曲并发布它。

所以,现在假设你已经说服自己需要设计一个新的应用,并且你有一些 种子概念 (seed concepts),比如 q&arecording (录音)。为了使应用连贯,还需要哪些额外的概念?显然,既然你依赖于众包,你会想要一些实现共识的概念,因此你可能会添加 upvote (点赞/赞成票),以便用户可以赞成一个答案。

当你头脑风暴你的应用程序将如何被使用时,你意识到你将需要以某种方式整合鸟类的识别结果。用户很可能想搜索特定的物种,并收听那些已经确认匹配的录音。因此,你尝试性地添加了一个 identification (识别)概念,尽管对其具体如何工作还不确定。也许当用户回答一个问题时,他们可以通过插入标签(hashtag)来提出一个识别,并且你的应用程序会自动从答案的点赞中提取物种和录音之间的链接。 最后,你决定添加 user (用户)来对贡献进行身份验证。此时,你得到了一个应用的粗略轮廓:BirdSong 0.1。

通用概念的清单

到目前为止,我们拥有的概念及其目的是:

  • q&a: 支持社区对问题的回应
  • recording: 允许上传音频文件
  • upvote: 基于个人的赞成(或反对)来对贡献进行排名
  • identification: 支持将对象分配到类别的众包工作
  • user: 验证内容和操作

请注意,每个概念都已被赋予了一个通用目的。当然,在特定应用的上下文中,每个概念都会被专业化。对于 BirdSong 0.1,问题将是关于鸟叫的;音频文件将包含鸟叫声;被点赞的贡献将是提议的答案或识别。甚至那个似乎非常针对鸟类的 identification 概念,也已被赋予了更通用的术语,希望这能更容易地从相关概念(比如 Facebook 中的 tag )中吸取灵感和经验教训。

将概念变得通用,不仅使得重用以前应用程序的设计知识成为可能。它也有助于简化设计。概念越少依赖于具体鸟类特性,它就越容易理解。例如,也许你很想在 identification 概念中包括鸟类是雄性还是雌性。然而,从先验角度来看,这是一个坏主意:在你拥有更多经验并理解应用及其使用方式之前,没有理由相信包含这种区别(而不是仅仅将雄性和雌性当作分开的鸟类对待)比任何其他区别更重要。如果你想将不同的鸟类关联起来,一个更合理的起点是通过增加一种鸟类 相关的 概念来丰富 identification 概念;然后这将不仅能适应性别的区分,还能适应该物种内的其他变体。

概念依赖图

由于每个概念都是通用的且自立的,因此在传统软件工程意义上不存在概念对概念的依赖关系。但是,概念之间存在一种不同的依赖关系,这与概念本身无关,而是与它们在应用程序整体中的角色有关。

upvote 概念为例。显然,除非有可以点赞的东西,否则在应用程序中加入这个概念是毫无意义的!可以说,被点赞的是对问题的回答(“那是一只#麻雀在唱歌”)。

所以我们会说 upvote “依赖”于 q&a,因为如果 q&a 概念缺失了,就没有理由包含 upvote 概念了。将所有这些依赖关系收集在一起,就得到了图 7.1 的图表。

有时候,一个概念的存在可以由其他几个概念中的任何一个来证明其合理性。在那种情况下,我们将其中一个依赖关系标记为主要依赖(使用实线),而将其他的标记为次要依赖(使用虚线)。次要依赖代表了加入某个概念的额外但没那么有说服力的理由。

因此,userq&a 有主要依赖,因为包含用户身份验证的主要原因是确保问题和答案能够可靠地与个人关联;而且对 upvote 有次要依赖,因为这种身份验证也可以被用来防止重复投票。第二种用途则不是那么必不可少;我们可以转而使用 IP 地址或浏览器 ID 达到那个目的。

图 7.1 鸟鸣应用的概念依赖关系。实心箭头表示主要依赖,虚线箭头表示次要依赖。核心概念以粗体显示。

该图表告诉我们哪些概念是应用的核心,哪些可以被省略。因为每个概念直接或间接地依赖于 q&a,没有它应用就无法存在——如果它包含这些概念中的任何一个,就必须包含这一个。但它可以单独存在,没有其他概念来增强功能。不可否认,这将是一个相当弱的应用:缺少 user 意味着没有身份验证;缺少 upvote 意味着没有众包;缺少 recording 意味着问题将不得不用语言来描述歌曲,或者可能链接到网络上其他地方的文件。

任何概念的子集都可以构成一个一致的应用,只要没有依赖边指向该子集之外即可。所以,例如,q&arecordingupvote 构成了一个应用。相反,identificationq&a 构不成一个应用,因为 identification 依赖于 recording。在我们的应用的上下文中,identification 概念提供了反向查找功能:给定特定的识别,它将引导用户找到相关的鸟鸣。如果没有 recording,这一角色就无法实现。

因此,该图表不仅仅描述了一个应用,而是一个包含所有能从这些特定概念构建出的应用的完整家族——软件开发者会称之为“产品线”(product line)。每一个一致的子集代表了一个可能被构建出来的应用。

子集也可以代表开发的阶段。在开发过程中的任何一点,你都会希望实现一个一致的子集,以便能够将其作为一个连贯的单元来进行评估。如果你实现了一个包含 upvote 但不包含 q&a 的集合,你将很难制作出令人信服的演示或测试,因为将没有内容可以被点赞。

图 7.2 Facebook(左)和 Apple Safari(右)的概念依赖关系。

最后,图表提供了一个 解释顺序 (explanation order)。你无法一次性解释整个应用,所以你要按顺序逐个或两个地解释概念。但什么顺序是有意义的?依赖关系告诉我们如何避免在一个概念有了动机之前就引入它。因此,如果我们向新手用户解释我们的鸟鸣应用,以下顺序将是有意义的

q&a, upvote, user, recording, identification

但这却不是

upvote, q&a, user, identification, recording

因为它在 q&a 之前引入了 upvote,而在没有可以点赞的东西的情况下你无法解释点赞行为(就像你无法演示它一样)。

一些熟悉应用的结构

为了进一步说明概念依赖图,以及它如何为设计提供深入见解,让我们看看一些熟悉应用的结构。

Facebook. 图 7.2(左)显示了 Facebook 的关键概念及其关系。基础概念当然是 post (帖子)。评论是针对帖子的,所以 comment (评论)概念依赖于 post 概念。reply (回复)概念提供了关于评论的主题式对话。user 概念主要为帖子提供身份验证,但也用于评论、回复、标签和点赞。

friend (好友)概念很有意思;由于其目的是允许用户控制对其帖子的访问,它不仅依赖于 user,还依赖于 posttag 概念涉及识别出现在帖子中的用户,因此它依赖于 userpost。最后,like (点赞)概念主要依赖于 post,但也用于评论和回复。

图 7.3 Apple Keynote 的概念依赖关系。

Safari. 图 7.2(右)显示了 Apple 的 Safari 浏览器的关键概念。如你所料,url 是基础概念;为了更容易布局,我把它放在了中间而不是底部。url 概念体现了这样一个想法:可以通过向具有持久名称(即统一资源定位符)的服务器发送请求来获取资源(即网页)。html 概念允许这些资源成为带有标记的页面,但大多数浏览器概念并不依赖于此,并且仍然可以在不包含 HTML 渲染的浏览器中(不可否认,是一个相当弱的浏览器)使用。cache (缓存)概念仅依赖于 url 概念;它通过存储之前对特定 URL 的请求所返回的资源来帮助浏览器运行得更快。certificate (证书)概念确保浏览器通信的服务器确实对应于 URL 中的域名,因此仅依赖于 url 概念。private browsing (无痕浏览)概念提供了一种不向服务器发送 cookie 的模式,保护了用户的身份,因此它依赖于 cookie.

在 Safari 图表的顶部是 bookmark (书签)概念及其三种变体:favorite (个人收藏),它像 bookmark 一样允许你保存 URL 以便日后访问,但会在工具栏和你打开的每个新标签页上显示这些 URL;frequently visited (经常访问),它会自发地从你多次访问过的站点创建书签;以及 reading list (阅读列表),它也像书签一样,但会跟踪页面是否已被阅读(并会下载该页面以供离线阅读)。这些非常相似概念的激增,以及它们之间的微妙差异,表明了进行更具协同效应设计的一个机会。离线访问页面和将页面标记为已读的能力可以被添加到所有书签中。而且经常访问的站点,像个人收藏一样,可以作为一个特殊的文件夹添加到常规书签中,以便在不需要时可以删除它们。

Keynote. 图 7.3 显示了 Apple 的幻灯片演示应用 Keynote 的关键概念。正如你所预料的,slide (幻灯片)概念处于基础位置。special block (特殊块)概念概括了标题、正文和幻灯片编号,这些是可选地出现在每张幻灯片上的,并在 master (母版)幻灯片中被赋予默认格式。theme (主题)概念允许在不同的演示文稿之间共享一组母版(为了保持一致性和易用性),并自然地增强了 text style (文本样式)概念(通过扮演一个 stylesheet (样式表)概念的角色,该概念将一组样式汇集在一起,以便在不同的文档中重复使用)。

除了 special block 概念外,还有一个单独的 text block (文本块)概念,以及一个 shape (形状,它也可以包含文本,但不会自动扩展以适应内容)。文本总是由 paragraph (段落)划分。标准 style (样式)概念有两个实例,一个用于段落中的文本,另一个用于形状。layer (图层)概念支持形状和文本块的堆叠(带有“置于底层”和“置于顶层”的动作)。animation (动画)概念主要支持在特殊块中逐步揭示要点,但也可以安排形状和文本块出现的顺序。

经验与实践

本章的一些经验:

  • 概念是自立的,且相互独立:一个概念可以独立被理解、设计和实现。这种独立性是概念简单性和可重用性的关键。
  • 在软件产品的上下文中,会出现依赖关系——这不是说一个概念为了其正确操作而依赖于另一个概念,而是因为只有在另一个概念存在的情况下,包含某个概念才可能是有意义的。
  • 依赖图提供了对产品概念及其被包含动机的简洁总结。它有助于规划设计和构建的顺序,识别子集,并构建解释。

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

  • 当你设计一个应用时,考虑每次以一两个概念的速度去发展它。在开始时,识别出几个种子概念,它们将构成所有后续发展的基础。
  • 画一个依赖图,以获得对你的应用中概念及其关系的简明视图。每次向你的设计添加一个概念时,都要仔细考虑它依赖于哪些概念:依赖项通常越多越好,因为这意味着该概念被更广泛地使用。
  • 在考虑按什么顺序来对概念进行原型设计或构建时,请参考依赖图,以便在任何时候你都能拥有一个连贯的子集。
  • 要探索简化应用的可能方式,可以评估各个一致的子集并估计每一个带来的价值。也许有一个子集能以很小的一部成本带来大部分的价值。
  • 当编写用户手册或开发培训材料时,使用由依赖图定义的排序,以最有效和合理顺序来展示概念。