完成用例
首席技术官,NetReliance
2001 年 5 月
本文是有关我编写的 OO 设计过程的系列文章的继续。前七个部分涵盖了规划阶段,从初始设计到问题陈述的细化以及开始使用用例。在下个月转向用户界面以前,我将在本月结束用例的讨论。
我在本月这篇文章中接着上个月继续填写“存款”用例的这个用例模板的剩余部分。和上个月一样,我不仅填写了模板,还提供了工作时思考过程的详尽注释。这个模板在本月的部分在某种程度上是用例描述真正的实质部分,因为它实际上描述了随着用例进展的工作流。当用户通过用例操作时,这个工作流就是程序在运行时所必须做的。
在我的上一篇专栏文章中,讲述了大约一半的用例模板内容。我将继续该话题。
填写“存款”用例的用例模板
依赖性
类似于:2.0。小孩从帐户取钱。(等同?)前一用例:4.0。家长开户包含:6.1。家长批准一笔存款后一用例:6.0。家长批准较早的存款。
这个列表是暂定的。例如,用例 2.0 的“类似于”关系实际上可能是“等同”关系。
先决条件
帐户必须存在。
输入
小孩挣得了一些钱。
方案
(“一路顺风”)
Philip 干完了当天的家务后决定将挣得的 2.25 美元存到 Allen 银行。(如果他每天做家务的话,我现在可能就破产了。)他进入银行,向出纳员要一张存款单,填完存款单后把存款单和他的存折一起交给出纳员。出纳员在接受存款单之前要求某种身份验证,然后将存款单交给银行主管人批准。银行主管人(家长)在存款单签名批准存款,然后把它交还给出纳员,出纳员再更新 Philip 的存折以显示此次交易。然后出纳员将存折还给 Philip,他便可以愉快地估算自己正在增长的存款余额了。
请注意在这个方案描述中,我小心谨慎地使用了问题陈述(在本系列文章中的前面的部分中讨论过)的词汇(例如,“存折”、“出纳员”等等)。只要我在这里引入了新的概念 — 那种情况确实会发生 — 我就必须回过头修改问题陈述来引入新的词汇。设计者的一个重要的(也是困难的)目标就是使所有设计文档能相互保持“协调”。如果一个文档中的更改影响到另一个文档,那么这两个文档都必须立即更改。例如,我有意不说“出纳员制作一个反映储蓄交易的日志项”,因为“日志”不属于这个领域的词汇而“存折”属于其中。对于设计而言,把它称做“日志”或“存折”没有区别。
还要注意我没有使用“系统”一词。“系统”不做任何事情;所有的工作都由扮演着“精心设计的”角色的参与者完成。某些参与者(小孩)是真人。另一些参与者(出纳员)是完全自动化的。还有一些参与者(银行主管人)在有些情况下是自动化的,在有些情况下是真人(银行主管人的某些职责由家长而不是计算机执行)。重要的是“角色”。至于扮演“角色”的参与者是不是自动化的并不是问题的实质。
在这个方案里,钱实际上没有加到帐户里,更准确地说,这笔存款是以“未批准”的状态输入存折的。任何家长可以在随后的时间里批准该交易。换句话说,存款在批准以前处于“扣留”状态。出纳员将存折还给小孩,并且在存折上会反映出这笔存款,但是在家长在存款单上签字以前这笔钱不能从帐户取出。(这个方案与“一路顺风”不同之处在于没有家长来批准存款。)
在这个方案里,家长为小孩存钱(不同于由小孩开始然后家长批准的交易)。
在这一时刻,和其它不太冒失的词相比,我觉得“小孩”这个词相当不妥。我之所以还用小孩这个词 — 而不是用孩子、客户或其它等价的术语 — 是因为在整个设计文档集中保持命名的内部一致性是很重要的。但我还是希望我选择术语时能更谨慎一点。
第二个方案实际上引入了一个新的用例,这是我以前没有意识到的:“家长批准优先于存款”。这确实是一个独立的用例,即使批准在当前用例中也是一个操作(意味着一个子用例:家长批准一笔存款)。
工作流
这部分是一个关键,所以我将使用与到目前为止我一直使用的略有不同的格式并且显示工作流是如何发展的。首先,我将“一路顺风”作为一个简单列表建模。
“一路顺风”方案:
小孩从出纳员领取存款单并填写。
小孩将存款单和存折一起提交给出纳员。
出纳员验证小孩的身份是否有效。
出纳员将存款单交给银行主管人以获得批准。
银行主管人将存款单返回给出纳员,标记为批准。
出纳员在小孩的存折里写一条记录以显示这笔存款。
出纳员将存折交还给小孩。
尽管对于简单的工作流线性列表非常有效,但当工作流复杂的时候它就不能满足要求了。
Constantine 和 Lockwood 建议,表示非线性操作时使用象“以任何次序:”后面跟一个列表。还可以用“同时地”后面跟一个表示并发的列表。
可能我是那种喜欢形象化的人吧,我发现这些仅用文字表示用例会使人糊涂。当用例很复杂时我更喜欢 UML 工作流程图。“一路顺风”的 UML 如图 1 所示。
图 1. “一路顺风”工作流程图
符号差不多是自解释的。每个框表示一个活动,箭头表示工作流。从实心圆开始并以空心圆结束。这些列,被称为“ 泳道(swim lane)”,标识负责执行活动的对象(或子系统)的类。
在看图时,我注意到有些地方可以有并行性。即,某些活动(验证小孩的身份和批准存款)可以并行完成。(您也许会发现,倘若在进行到下一步之前这些活动都被执行了的话,则它们执行的顺序并不重要。)
图 2 显示了为反映并行性而对图 1 所做的修改。表示一组并行行为开始的粗水平线(在表示这组行为的流程图的顶部)称为“ 分叉”(fork)。底部的线(表示到达这点时,这些并行行为在继续后一步可能的工作之前必须要同步)称为“ 汇合”(join)。
图 2. 经过修改的“一路顺风”以反映并行性
现在我回过头考虑另外两个方案,并试着把它们合并到“一路顺风”图中。(修改后的图见图 3。)通常,如果不可能合并这些方案或合并后生成的图显得非常笨拙,那么这些附加的方案可能应成为独立的用例,而不是当前用例的方案。当然,目前这几个方案合并得很好。
在我意识到倘若我允许家长扮做小孩的角色那么工作流就和“一路顺风”一模一样以后,解决家长为小孩存款这一方案就不费吹灰之力了。就是说家长的密码可以用来登录小孩的帐户。我将这一结果的注释放在活动图(图 3)和商业规则部分(在下面)。
第二个方案(家长不在场)稍稍有些困难,因为我必须引入分支(branch)(在图 3 中以有一个输入箭头和多个输出箭头的菱形表示)。控制沿哪个输出路径而行的条件(称为“ 监护”(guard))就是在线上的文字标签。合并(merge)(也由一个菱形表示,但有多个输入箭头和一个输出箭头)标记条件行为的终止。每个分支应该有对应的合并。
可以如下表达在“分叉”与“汇合”之间的所有复杂的情况,如下:验证小孩的身份并在家长在场的情况下标记存款为批准,或者当家长不在场时标记为未批准(这两个过程不分先后次序)。当两个活动(验证和标记)都完成后就更新存折。
请注意,最初的方案描述说明出纳员扣留未批准的存款。当使用这个图时,我们可以明显地看到将未批准的存款扣留在存折中比由出纳员扣留方便得多,于是我就那样做了。我回过头修改了方案描述以反映这种更改。
图 3. 将附加方案添加到“一路顺风”
后置条件
新帐户余额 = 旧帐户余额 + 存款额
请注意这个用例包含了两个不必同时发生的不同操作(存款请求和存款批准)。然而,这的确是一个具有单个结果的单个用例。帐户余额总会改变,即使存款活动与批准活动之间相隔数日。
输出
更新的存折。
这一部分将是显示输出形式的地方(例如,以报表的形式)。我将这一部分推迟到随后文章中开始解决 UI 的时候再讲述。
商业规则(与领域相关)
存款生效前必须由家长批准。
利息自存款当日计算,即使几天之后才有批准。
未批准存款能无限地处于未批准状态,等待家长签名。它们不会“过期”。
家长的密码将验证小孩。即家长可以作为小孩登录。
需求(与实现相关)
存款必须自动“保存”。这里没有“File/Save”菜单。显式地“撤销”没有必要也不需要。可以用取款来撤销存款。
当小孩看存折时,必须显示“扣留”的存款。以灰色显示扣留的存款。
如果我说的是“以某种方式突出显示”而不是“以灰色显示”,那我将冒险写下无意义的规范。到结束时,规范必须明确。既然最后都要做出决定,那么不妨现在就开始。如果做了错误的决定那么总是可以回过头去做更改(当然是在规范正式批准以前)。
对我来说做错误的决定总比不做决定要好。我看到过的许多所谓的规范都太不直接明了,以至于可以从这个规范生成几百个不同的程序,而且所有的程序都是符合这个规范的。这种不明确性比无效更糟,因为它促使您做一些不正确的工作。
在当前用例中,我想这个问题显得太琐碎,以至不必作决定。只要对这一项 做了突出显示,至于如何做就并不重要了。因而这个决定可以推迟到正式的 UI 设计过程。如果突出显示的概念模糊不清,那么实现设计的各个程序员将按各自对突出显示这一概念的理解实现,导致 UI 不一致,其结果是更难使用。
实现注解
目前还没有登录的概念。看着这些方案,Philip 不大会一次做几项交易。事实上,更有可能是他只做一项交易,但是忘了注销。(这是我在家里观察到的行为 — Philip 经常在早上玩游戏机,而我会发现游戏机在 12 小时之后还开着,也就是在他上床睡觉之后。)考虑到这种行为,接受存款单的同时还要求某种验证似乎更安全些。这也是真正的银行的本来的运作方式。
与登录类似的是在被允许进入银行之前向警卫表明身份。(那也许是一个合理的方法,只不过我没有选它。)请注意在这个用例中没有禁止出纳员记住 Philip 是谁。也就是说,客户在这个用例的作用域内以某种方式被标以“可信的”,这样不需要重复验证过程就可以执行随后的交易。“可信的”属性可以通过注销来取消,或者干脆在给定时间到了以后,取消这个属性。(例如,如果在银行您两分钟后仍无任何活动的话,将变成不可信的。)
(在我们开始真正的对象建模之前,您可能会不明白这个注解,所以如果现在不理解,不必担心。)在大多数情况下,小孩是外部对象。也就是说,系统里没有程序性的对象表示小孩;更准确的说,小孩是系统的物理用户。另一方面,既然系统里某个对象必须处理验证,那么小孩身份的概念几乎肯定会被建模。
“可用资金”与“当前余额”之间有所不同。(后者不反映未批准交易。)既然小孩认为存款就是存款,不管是否批准,存折应该把二者的值都显示出来。不管怎样,告诉小孩“扣留”的工作原理是没有坏处的。
我还没确定怎样在 UI 表示扣留才最好。解决方案之一是在存款单上向查看存折的小孩显示扣留,但在存款单获得家长批准之后存款才会在存折上正确地显示。另一个可能的方案是存款象获得批准的存款一样显示在存折上,但带有表示存款正被扣留的某种可视化标记(灰色文字?)。
后者更好一些,但这意味着“家长批准当前存款”的用例将可能与“家长批准暂挂的存款”的用例有明显的不同。
请注意,如果出纳员为家长批准而扣留存款单,小孩将不得不请出纳员看存折,以便于出纳员可以显示扣留的存款单和存折。
解决家长批准的最好方法可能就是 UI 上的存款单本身。或许有一个“批准的”框,在选中后会弹出一个小的对话框用来输入签名。另一个方法是在存款单上有一个“批准签名”栏,家长可以在其中输入“同意”这类短语。(请注意交易已批准的指示 — 例如,签名的副本 — 应该出现在经过批准的存款单上)。请不要在“经批准的签名”栏中显示 x(或至少做到:一旦签名被验证就替换掉这些 x);在通过验证的阶段泄露字符的数目实在没有必要。
这里有一些访问控制问题。扮演小孩角色的参与者能指定存款,但不能批准存款。(批准会使帐户余额和存折被更新。)扮演家长角色的参与者既能指定存款也能批准存款。系统必须能够区分这两种角色的参与者 — 密码验证就足够了。
扣留资金的利息会被累加,即使还不能动用这些钱。处理利息可能的最好办法是把存折看作交易的列表,按日期排序。当存款单被批准后,将把它按已排的顺序插入到交易列表中。利息将从最近插入的交易的时刻开始重新计算。若用这种方法,则未批准的交易实际上没有生利息。
然而您也许想根据天数来显示利息的增长,那么象真正的银行存折或银行帐单那样只列出交易就显得不够。您实际需要每天有一行项目。但是会有很多行。一个合理的折衷办法是在每天计息的基础上显示前 30 天的信息,而存折剩余部分则象标准的银行帐单一样按常规显示公布的利息(不是每天),可能每星期公布一次。
这就是完整的用例介绍。为了不使您感到厌烦,我不在随后的文章里介绍用例的剩余部分,我将把设置帐单的整个过程放到我的网站上(请参阅参考资料)以便您在空余时间浏览。下个月我将继续这个设计过程:研究 UI 设计(它是必不可少的,不仅因为我们将必须构建它,还因为 UI 能用于验证这些用例和提供有用的通信工具)。