Allen Holub
首席技术官,NetReliance
2001 年 1 月
在前几篇文章中,我们已经细化了问题说明,并且建立了教育软件的模型。在本文中,我们将研究用例分析。
本文继续探讨我的 OO 设计过程系列。前四部分是:
这个月,我将分析程序的动态(运行时)行为。为了进行分析,我将引入用例的概念,并且将讨论如何构建其中一个用例。下个月,我将把这个结构放到“艾伦银行”的框架中去。
用例简介
“问题说明”现在看似很好。我可能已经忘了一些关键性的东西,但傻坐着一点用也没有,不要指望这些丢失的东西会从我的脑袋里完完整整地蹦出来,就象雅典娜从宙斯的脑袋里出来一样。通过继续前进并以另一种方法研究同一个问题,这些丢失的东西会逐渐变得清晰。然后,下一步就是再一次研究问题,但这次是从动态(例如,运行时)行为的角度来进行研究。在某种意义上,问题说明是问题的 静态定义 -- 它描述了用户必须解决的问题,但仅此而已。最后,我将以静态模型的形式来使这种系统的观点成为正式。
问题的动态定义关注的并不是必须做什么,而是如何去做。即,与其研究问题本身,还不如研究用户为解决问题而必须做的事。我们用来在动态行为上构建思想的主要工具就是 用例分析。还可以以动态模型的形式来表示动态行为,但在那样做之前,我们必须确定动态行为 是什么。我们将使用用例分析来做这些。
首先,定义:
用例是单个任务,能产生有用的结果,由系统最终用户执行。
“最终用户”和“结果”的定义可以多种多样 -- 例如,最终用户可能是自动化子系统,而结果的种类也可以随着用例的不同而有着根本上的不同。有时结果是实际产品,如报表,而有时结果是公司目标,如雇佣一个雇员。但结果始终有一个可以感知(由用户)的值。
可以认为用例是符合用户意图的。如果用户进入系统是为了做某些事,那么他要做“什么事”呢?例如,登录就不是完整的用例,因为没有人会仅仅想要登录而进入系统。您登录是为了实现某些其它更大一点的目标,要执行某些实用的任务,而用例正是那个更大任务的定义。
表示用例
正如软件中的常见情况,一个经过整理的半正式用例表示有助于您建立用例。正式的表示通常涉及表 1 中列出的几个部分。(我待会儿就会解释每个部分包含的内容。)最好将表 1 看作是一个检查表,而不是一个往里填补空白的表单。并不是所有部分都与每个定义有关,在实际工作文档中可以组合(或忽略)任何部分。
而且,虽然必须提供表 1 中的所有类别,但却无需按该表中指定的顺序提供它们。通常,最好是从方案开始,将它们开发成正式的工作流定义,然后填入其它细节。表 1 中的顺序实际上是为了方便用例的实施者,而不是创建人员。
我们设计人员在创建用例的过程中有几个主要目标:
- 完善对问题逐步深入的理解。
- 与最终用户交流,以确保我们真正在解决他们的问题。
- 动态地组织设计(和最终程序),以反映用户头脑中真正的商业模型。换句话说,确保程序真正能做一些对实际用户有用的事。
- 对实际开发过程提供指导说明和结构框架。(在几个月后我们开始讨论实现时会详细说明这一点。)
名称 |
用例定义是协作过程。这些组成部分中有许多是由商业方(通常是市场营销)组织指定的,而其它部分由技术方组织填写或扩充。用户界面设计也是用例分析的一个重要组成部分。UI 模型(不是原型)提供了许多关于是否已正确捕捉到用例有用的反馈信息。下个月我再讨论基于用例的 UI 设计。
现在,让我们展开表 1,详细地说明文档的各个部分。
名称
所有用例都应该有名称。Constantine 建议使用动名词,后跟一个直接宾语(例如:“取款 (withdrawing funds)”或“检查存折 (examining the passbook)”)。这种约定促使用例名称简明地标识所执行的操作以及受该操作影响的对象(或子系统)。我认为此建议就现状而言是好的,但如果您最终希望不遗漏重要信息,那么它对于恪守公式却没有特别好处。例如,“客户从活期帐户中取款 (customer withdrawing funds from checking account)”与“银行经理从客户的活期帐户中取款 (bank manager withdrawing funds from customer`s checking account)”就不是同一个用例。简单的“取款 (withdrawing funds)”这个名称就不能充分说明这一点。
名称应始终以用户为中心,而不是以系统为中心。例如,“存款”(以用户为中心)与“接受存款”(以系统为中心)相反。从用户的角度来命名,而不是以系统的角度命名。
不论是哪种格式,名称是至关重要的 -- 必须明确地标识用例,以便能够有效地讨论它。我通常还对每个用例指定某些易于管理的身份,这样就可以方便地从其它文档引用它。通常,我更喜欢使用非常一般的描述,再加上有层次结构的编号方案(例如," Withdrawal: 1.1")。
描述
描述用例实现什么。例如,当“取款”或“检查存折”时,用户要做什么。应详细说明,但不要描述用户如何使用计算机程序。
例如,银行客户在取款时会填写一张取款单,并向银行出纳员提供这张取款单。然后出纳员将此取款单交给银行主管人以获得批准。银行主管人检查帐户余额并正式批准,等等。请注意,在此论述中我从未提到过计算机程序、菜单、对话框等。与在这个层次上,这些实现细节都是 无关的。(当然,虽然在开始编码之前仍需要明确的实现细节。但我们还没有开始呢。)
我对提到“系统”的分析级文档感到特别头疼。它也许是您在创建某种“系统”时遇到的问题,但它肯定不是您用户的问题,即不是在这里显得很重要的用户的问题。
期望结果
根据定义,用例应该具有有用的结果。必须执行某种有意义的工作。在此描述结果。结果也许是一个报表(在此情况下,应该包含一个报表外观的示例),一个事件或条件(现在雇员将得到医疗津贴),或者类似的东西,但必须有一个有用的结果。
用户目标
用户使用用例的真正目标是什么?请注意,目标与用例描述并不相同。如果“期望结果”部分描述了用户希望实现 什么,那么“目标”部分就描述了用户为什么要得到这个结果。
知道用户目标可以从根本上影响用例的发展方向。借鉴一下 Alan Cooper 的例子,假设我们负责创建会议日程安排程序。让我们感兴趣的用例是“安排会议”。您的脑海中会立即浮现出日历和约会本的图像。但那种方式类似于 Microsoft Outlook -- 一个臃肿但难用的程序,它实际上不能为任何人做什么有用的事(艾伦,现在告诉我们你到底在想什么)。
那么,如何避免使我们的系统象 Outlook 那样呢?要考虑目标。每个人对于会议的期望是什么?我保证我们都有同一个目标:根本不要去。除此之外,我们的目标就是尽快离开,并使会议尽可能有结果。实现此目标的唯一方法是制订一个议程。因此,“安排会议”用例的第一步就是子用例“创建议程”。(马上就将详细讨论子用例。)而真正确定日期和时间就变得次要了。
参加者/角色
用例中的参加者不是自然用户,他们是可能与系统相关的自然用户的角色。例如,我们正在建立一个考勤卡授权系统,那么会立即想要两种角色:雇员(填写考勤卡的人)和经理(核准考勤卡的人)。事实上,同一个自然人在某一时刻扮演两个角色并不冲突,因为有两个 逻辑参加者:雇员和经理。
UML 将这些角色定义成“参与者”。但是,我经过慎重考虑,决定不使用该术语。我认为,实际用户是扮演与系统相关的角色的参与者。参与者通常在系统外部,与系统无关。真正重要的是参与者扮演的角色。(并不是我一个人认为“参与者”并不是好的术语,见参考资料。)
关于参与者在系统外部的规则有一个特例。设想关于访问控制的问题:假设您正在鉴定一个以给定角色在程序中进行操作的参与者。因此,参与者出现在程序中,将参与者映射成角色才能使程序工作。
CRC 卡 对于每个角色,我们需要建立两段关键信息:
|
这些类别是 CRC(表示“类/责任/协作者”)卡的两个组成部分,它们用于某些 OO 设计方法,如 Kent Beck 的极端编程。(CRC 卡由 Beck 和 Ward Cunningham 开发,用于教学辅助;见参考资料。)
由于此信息与参加者紧密联系,而不是明确的用例(即,某个扮演特定角色的参与者也许要参与几个用例),因此有时将 CRC 卡当作根据个别用例引用的单独文档进行维护就显得很实用。由于我们将进行 UML 动态建模,因此维护责任列表将会显得非常重要。
依赖性
依赖性关系有时并不存在 -- 但当一个程序相当复杂,且有多个用例时,那种情况就不多见了。通常依赖关系不会立即显现,但会随着用例模型和相关用户界面的进化而逐渐显露。
典型的依赖关系包括以下一个或多个关系:
- 子集/组合
通常在开始分析顶级用例并发现可以通过执行几个较小但独立的任务来实现复杂任务时,会出现子集用例(我称作subcode
)。这些独立任务中的每一个都是主用例的一个子集。无论使用“分解成子集”还是“组合”,实际上这只是开始时的一种方式。如果您从较小的用例入手,然后意识到可以将它们组合成较大的用例,那么就使用“组合”。如果从较大的用例入手,然后要分解它,那么请使用“分解成子集”。例如,虽然标识自己(登录)通常不是本意想做的用例(您不会仅仅为了要标识自己而进入系统,然后关机、回家),但它可能是其它用例的子用例。而且,在所有需要标识的用例中,标识过程也许是相同的。应当创建“标识自己”子用例并将它指定成类似于独立用例,然后通过在主用例中进行引用来合并它。 - 使用/被使用(包括)
这一关系非常类似于子用例关系;不要浪费许多时间来担心“使用”和“子用例”关系中哪个更好,因为在使用用例文档时这两种关系的处理方式是相同的。“子集”和“使用”之间的主要区别是当用例既是子用例也是独立用例时应用“使用”关系。(它定义了一个产生有用结果的独立操作。)子集型关系有时用于区别由另一个顶级用例使用的子用例,而“使用”关系也许描述了一个在所有场合使用的子用例。 - 前/后
这一关系建立了用例之间的工作流。例如,“注册客户”必须在“指定订单”或“浏览商品目录”之前。 - 需求
前/后关系表示顺序,但不是依赖性。即,“购买购物车中的商品”用例必须要使用“注册用户”,但“注册用户”只是在“浏览商品目录”之前 -- 它并不是一个必要条件。 - 扩展/专属
如果用例 B 扩展了用例 A(即,添加子任务、操作等),那么 B 就是 A 的一种特例。(通常,需要额外任务是为了满足某些普通用例中不会出现的特殊需求。)例如,“标识经理”可能属于“标识雇员”的一种特例,因为经理必须被鉴定拥有比普通雇员更高的安全性标准。(程序员将这种 专属关系称作派生,他会说 B“源自”A 来表示 B 是 A 的特例。) - 类似
通常,您会注意到工作流中的两个用例相互之间很相似,尽管它们之间还有一些小差异。 类似关系表示您应该密切关注相似的用例,尝试查找共同点,它们可以被分解成“子集”用例或等价物。 - 等价
从用户的角度看,两个用例可能不同,但它们的实现方式却相同(在“艾伦银行”示例中,等价用例之间的逻辑区别)。基本代码是否相同与用户并无关系 -- 存款和取款就是不同的逻辑操作。在任何事件中,等价用例在是设计过程中都有分路。
我发现使用 UML 静态模型图表示这些依赖关系非常有用。(换句话说,UML 的正式用例标记法几乎毫无价值。)Constantine 和 Lockwood 已经提议用图表表示用例关系的表示法,但我更喜欢使用标准 UML,当暂时没有与现成表示法可以使用时,标准 UML 会使用固定形式。以后我会给出一个具体示例(在“艾伦银行”项目中)。
前置条件
当运行用例时,关于系统的状态,您会做出什么假设?例如,用户在取款之前必须在银行有一个帐户。因此,必须先执行“客户开设帐户”用例,然后才能执行“客户取款”用例。
在可以成功完成此用例之前,必须存在什么条件?条件可以是内部的,也可以是外部的。例如:帐户余额必须大于取款额。
方案
方案是关于某人使用用例的简短叙述性描述。应从旁观者的角度去描述:作为观察事件发生的旁观者描述发生了什么。
既然是描述,我尝试尽量使方案抽象(讨论如何使用银行,而不是模拟银行的计算机程序)。“Philip 需要取款来购买食品。他从衣柜最上层抽屉里的一大堆脏袜子下面翻出存折,发现存款余额足够满足他的需要,然后前往银行……”
某些程序员认为方案毫无价值,因而不使用方案,但我却发现它们非常有用。最重要的用途是理清我的思路。通常,当完成方案时,我会发现未曾想到过的用例,或者我会发现未曾出现过的工作流问题。例如,其实在取款之前就会发生一些问题。(要是他不能找到存折,该怎么办?)
设想一个可能有几个有关方案的用例。在最近由我主持的 OO 设计专题学术讨论会上,我们选择 Vanderbilt 大学课程登记系统作为样本项目。其中只有一个高级用例,即:“课程登记”。可是,在这个用例中,我们想到了几种方案:
- 我坐下,报名参加我要学的所有课程,加入这些班级,我很满意。这种方案 -- 一切畅通无阻 -- 称作(至少我这么叫)“一路顺风”方案。
- 我报名学习要学的课程,但有一个班已经满了,因此我被放入了候补名单。以后,这个班有了一个空名额,我就被自动登记,并会通知我参加该班的学习。
- 与第 (2) 种方案相同,但我一定要学习此课程才能毕业 -- 我必须加入这个班。
- 其它方案
在分析这些方案时,我会判定第二和第三种方案实际上包含了一个与“一路顺风”方案截然不同的独立用例。
请注意,某些方案是失败方案,但不应该将这组方案看作只包含了一个“一路顺风”方案和许多失败方案。在课程登记示例中,一个真正的失败方案是您必须学习某个课程才能毕业,但却不能加入这个班。
实际上,在大多数已正确完成的用例中没有失败的方式!起先,这种惊人的论述并没有如预料中那样令人震惊。大多数计算机程序采用错误消息作为掩盖程序员偷懒的方式。实际上,大多数“错误”可能由程序本身就能处理。然而由于程序员懒得寻找处理错误的方式,因此大多数情况下会打印错误消息,这样他们就将这项工作推给了用户。以“班级已满”为例。懒惰的设计人员会说“哦,您要参加的班级已经满了。太可惜了。我无能为力”,并且会将这种情况归结为失败的方案:而出色的设计人员则会寻求解决方案,如我们的候补名单策略。作为设计人员就应该解决用户的问题,而不是把问题再还给用户。Alan Cooper 甚至说精心编写的程序根本不应该打印错误消息。我不敢肯定这个目标是否能够实现,但我敢打赌您可以消除大多数程序中 90% 的错误消息,最终会得到一个更便于使用的系统。
另外,对于程序员,请注意在用例中根本不要出现严重的致命错误 -- 这种错误会导致抛出异常。这些条件表示程序的失败方式 -- 它们与实现相关,但与问题领域无关,因此它们不属于用例分析。请记住,我们正在从用户的角度定义要解决的问题,我们不是在设计计算机程序(还没到时候)。设计程序要等到我们完全理解了问题以后再进行。异常抛出错误条件实际上是实现需求(见以下部分),应该在“需求”或“实现注意事项”部分中列出。
不要将上述的讨论作为借口而不考虑失败条件。您的目标是考虑所有可能的失败条件,然后确保每次出现失败条件时,它都会得到妥善处理。创建一个列出所有失败方式的工作文档不会有什么坏处。然后当复查用例以确保已经考虑了所有失败方式时,可以使用该列表作为检验表。
最后一个注意事项,销售人员在总结陈述时会发现方案确实有用。能卖出您正在构建的产品总是好事。
工作流
通常,用例描述足够描述一个简单用例中的工作流(“执行 A,然后执行 B,然后执行 C”)。有时工作流非常复杂,因此不应在“描述”部分中完整地描述它,而有时需要图表(如 UML 工作流图表,我会留在最后讨论)来明确定义工作流。在此填写此类信息。
后置条件
用例常常会更改系统状态(例如,现在帐户余额下降了),而不是生成实际产品。另外,需要在某些用例之后执行其它用例以便完成整个任务。此类信息都应填写在“后置条件”部分中。即,可以在用例完成之后检查后置条件,以便确定用例是成功还是失败。后置条件示例:新的余额就是老的余额扣除取款金额。
商业规则
商业规则是商家建立的策略,它可能会实现用例的结果。例如,“您每周取款数额可能不超过 20 美圆。”用例描述中最好不要包含这些规则,但应该以某种方式指定这些规则,以便使文档足够正确而能够使用。我发现最好在一个单独的文档中指定这些规则,然后在“商业规则”部分中引用这些个别的适当规则。
需求
需求通常是客户指定的关于系统实现的行为约束。(“必须支持每分钟 10,000 次交易。”)再次声明,最好在一个独立文档中指定并引用需求。
请注意,某些称作“需求”的东西其实并不真是“需求”。UI 设计通常是作为“需求”指定的,但真正的需求通常不是程序的外观如何,而是它做什么。如果实际用户不在意程序是否使用肾形嵌套紫红色按钮,那么肯定没有对使用肾形嵌套紫红色按钮的“需求”,不论您多喜欢这种按钮。(换句话说,如果用户在意,那么这就是需求,不论有多小。就是这样。)
我曾经有一个客户,他常说“用户需要[某些功能]。”我的回答是“哪些用户?告诉我他们的名字,这样我可以给他们打电话,和他们进行讨论。”由于实际上不存在这种用户,因此很容易忽视这些需求。这并不是说设计人员有时会不考虑有用的功能,但除非有一个真正的用户 关心您的创新,否则它们就不切实际。要迫使您的用户们接受某些糟糕的创新很容易,但这些创新什么也干不了,只会在用户使用程序时给他们添麻烦。应拒绝这种诱惑。
了解需求是否有效的一个好的测试是拒绝所有在设计过程的自然产品之前指定的所谓需求(UI 观感、程序结构等)。这种“需求”只是错误的下意识设计。从需求文档中除去此垃圾。如果您有训练有素的商业开发队伍,那么从一开始在此文档中就不会有这些“需求”。
最后请注意,OO 设计人员不可能从“功能”列表着手进行工作。功能列表可能会是很长但又紊乱的未经细心考虑的构思集合,这些构思是某些客户未经大脑考虑而向销售人员建议的。然后销售人员会把此建议转换成“哦,天啊,我们现在必须设计这个功能!!!”,然后我们就这样做了。他们最多不过是建议了研究方向,但他们自己也不知道是否正确。您感兴趣的是 为什么会提出这些功能。这种特定功能如何使用户的工作变得更简单?用户想要实现什么?您的其他客户是否想要实现相同的事?是否有更好的方法可以实现目标?最重要的是用户要执行的任务是否是我们以前没有遇到过的用例?
实现注意事项
尽管我们的目标是使用例文档尽可能位于问题领域中,但在完成方案和工作流时想到实现细节也是很自然的。当它出现在您的脑海中时,应该捕捉此信息,那么这一刻片段可以让您草草记下实现注意事项。这些注意事项并不具体。它们并不是实现规范。实际上,它们是将会影响实现以及与当前用例有关的细节。它们将指引实现级设计,但不会控制此设计。
结束语
因此,这就是用例的建立过程。下个月,我将为“艾伦银行”项目开发一些用例,从而向您演示如何应用此理论。以后的文章还将演示如何使用 UI 设计的用例,以及如何根据用例构建正式的动态模型。
参考资料
- Ivar Jacobson, Object-Oriented Software Engineering : A Use Case Driven Approach (Reading: Addison-Wesley, 1994; ISBN: 0201544350)。此书是最早谈到用例的书之一。虽然 Jacobson 是此领域中的先驱,但我和他在诸如用例颗粒度等事物的看法上还是有不同意见。可我还是列出了这本书,因为它是很基础的,但对我来说有写抽象。
- Larry Constantine 和 Lucy Lockwood, Software for Use (Reading: Addison-Wesley, 1999; ISBN: 0201924781)。此书描述了基于用例的 UI 设计方法,称作以用法为中心的设计。这种方法是最好的正式 UI 设计方法之一,此书的前四分之一收集了很好的用例讨论。
- Larry Constantine 和 Lucy Lockwood,Structure and Style in Use Cases for User Interface。此书概述并扩充了 Constantine 和 Lockwood 的著作中讨论的设计方法。它是关于该主题的一个很好的摘要介绍。
- Russell C. Bjork, An Example of Object-Oriented Design: An ATM Simulation。这些都是 Bjork 博士在 Gordon College 教书时出色的课堂笔记。虽然我对某些资料不太满意,但 Bjork 确实介绍了一套完整的源于 OO 设计过程的设计方法(包括用例)。
- Kent Beck 和 Ward Cunningham,A Laboratory For Teaching Object-Oriented Thinking。(摘自 OOPSLA'89 Conference Proceedings October 1-6, 1989, New Orleans, Louisiana,和 SIGPLAN Notices Volume 24, Number 10, October 1989 专刊。)此书描述了在教室环境中 CRC 卡的最初用途。
关于作者
Allen Holub 是 NetReliance 的首席技术官,该公司位于旧金山,它为在网络上执行可信任商业交易而构建安全的全局基础设计。
他自 1979 年以来一直在计算机领域工作,曾在杂志(Dr. Dobb 月刊
、程序员月刊
、Byte
、MSJ
以及其它杂志)上发表过大量文章。它为在线杂志 JavaWorld 撰写过 "Java Toolbox" 专栏,还为 IBM developerWorks 组件专区撰写过“OO 设计过程”专栏。他还主持 ITworld Programming Theory & Practice 讨论组。
Allen 有八本书广受好评,其中最新的一本讲述了 Java 线程的陷阱和缺陷 (Taming Java Threads)。他从事设计和构建面向对象软件很长时间了(用 C++ 和 Java)。他自从 1982 年一直在加利福利亚大学伯克利分校讲授 OO 设计和 Java。可以访问 Allen 的网站 http://www.holub.com 与他取得联系。