一错再错的这故事才精彩
——朴树 《我爱你再见》
摘要
即使读了再多的书、跟过再多的项目,到了需要自己创建领域模型的时候,还是感觉不知从哪儿下手。就像即使看过再多的小说,到了自己想写小说的时候,仍会感觉无从下笔……本文将给出3个实用的建模心法,并通过一个实际项目介绍如何应用USM三视图法迈出建模第一步。
殷六侠即将平生第一次下山做任务
殷六侠来到张三丰的禅房。
殷六侠:“师傅,弟子就要下山去了,特来向师傅告别。”
张三丰:“梨亭啊,虽然你平时读书很用功,也跟着师兄们做了一些项目,但这次毕竟是第一次独力建模,心里有没有谱呀?”
殷六侠:“说实话,弟子现在大脑一片空白。”
张三丰:“……”
殷六侠:“弟子虽然已经把《领域驱动开发心经》、《设计模式真经》、《分析模式真经》读了个滚瓜烂熟,可是到了用的时候,还是有一种无处着力的感觉。”
张三丰:“简直都不知道从哪开始对不对?这是很正常的。这次事出紧急,只能由你一个人立即赶往同仁堂,实在有些难为你了。不过好在现在科技发达了,有什么问题可以随时用QQ与为师联系。时候不早了,你快些动身吧。”
殷六侠:“那徒儿就告退了,师傅保重!”
张三丰:“等一等,你进禅房之前可曾听到为师鼓瑟?”
殷六侠:“师傅是否想说建模就如同鼓瑟,音乐的好听与否并不因为单独的某个音符的高低短长,而是所有音符连续起来的效果?”
张三丰:“好,很好。我的七个徒弟里面,除了你五师哥,就是你悟性最高了。快快下山去吧。”
殷六侠:“徒儿告退。”
第一天的QQ聊天记录
殷六侠:师傅,弟子已经顺利到达同仁堂,这边的领域专家用一上午的时间向我介绍了一下住院部的业务,我整理出了十几个用例:(限于篇幅,这里仅列出3个)
用例(use case) 由用户的目标联系在一起的一组场景。
场景(scenario) 一系列表述用户和系统之间一次交互的步骤。
名词法为什么行不通
张三丰:很好,用例做得挺不错的,接下来你打算做什么?
殷六侠:谢谢师傅夸奖,做用例其实并不难,只要把领域专家说的东西稍加整理就行了。弟子接下来就准备根据用例制作领域模型了。我想用名词法,首先找到用例中的名词,例如“收款员”就是实体,与它相关的动词,例如“提交入院登记”就是实体的一个方法。然后我再根据面向对象设计的原则和设计模式对类和职责进行调整。
张三丰:很遗憾,我不得不指出你犯了一个初学者常犯的错误——把用例中的名词等同于领域模型里的实体。事实上,用例代表的是系统的外观,用例中的名词和系统中的实体没有任何联系。
殷六侠:可是,领域模型里难道不该有一个叫“收款员”的实体么?
张三丰:没错,领域模型里会有一个叫“收款员”的实体,不过它和用例里面的收款员可不是一回事。用例里面的收款员处于系统外部,是一个活生生的人;而领域模型里的“收款员”实体,确切的说应该叫“收款员基本信息”,这个实体只知道收款员的姓名、性别、年龄和权限等信息。
殷六侠:我注意到您用了“知道”这个词,您是不是想说“收款员基本信息”实体知道得太少而无法承担“提交入院登记”这个职责呢?
张三丰:记得《蜘蛛侠》里面的经典台词吧?“能力越大,责任就越大。”对于类来说,知道的越多,操作就越多。一个类有2种职责:1)知识性职责,包括属性、关联和无副作用的方法;2)操作性职责,就是指类的含副作用的方法。一个原则就是,要把职责分配给最容易取得它所需的信息的那个类。
殷六侠:可是,建模的时候会面临着3个问题:“模型里要有哪些类?类之间如何关联?类有哪些职责?”应该首先考虑哪个问题呢?或者说哪个问题比较重要呢?
张三丰:记得有句古话叫“程序=数据结构+算法”吧?那么你说是先有数据结构呢?还是先有算法?
殷六侠:若要为解决某个问题设计一个算法,虽然有可能会先考虑数据结构或算法,但是其实它们两个是相互配合、无法单独工作的吧?也就是说,它们在理论上应该是同时产生、同等重要的吧?
张三丰:没错。而类是把数据结构和算法捏到了一起,所以理论上这三个问题也是被同时解决的。
殷六侠:可是恕徒儿愚笨,要同时思考这三个问题我可实在是办不到。
张三丰:好在实体类还有一个更重要的职责:它要具有延续性和生命周期,并且以identity而不是其它的属性来相互区别。我们要首先按这个职责来构建实体和关联,然后再考虑实体的其它职责,这样就简单多了。
殷六侠:那么设计模式是否对找出实体有所帮助呢?
张三丰:应该说用处不大。因为建模的目的是构建一个领域模型来仿真现实的业务,它的结构恰巧与设计模式里的类结构相同的情况并不多见。需要注意的是,设计模式关注的主要是如何应用OO的技术手段(接口和多态)来简化设计和增加弹性,它们的关注点是不同的。现实业务里组合的情况很常见,例如合同包含一些产品,产品由部件组成,但是却不一定需要使用Composite模式那样的类结构,因为可能并不需要Composite模式所提供的那么强大的一致性和弹性。如果勉强使用Composite模式反而会使模型难以理解,还会由于使用了过窄的接口导致大量的向下转型操作,为Client代码增加了不必要的复杂性。设计模式确实提供了诱人的一致性和高内聚性,但是你得首先找到领域中的一致性才行。
殷六侠:那我该如何找出实体类呢?实体实体,就是实际存在的物体吧?我注意到每个患者床上都挂着一个床头卡,那么模型里应该有一个床头卡实体吧?还有我可以去收集所有的报表,然后从这些报表里的字段来分析出该有哪些实体。
张三丰:我希望你从一开始就有一个清醒的认识:建模是一项无中生有的、100%的创造性工作,并不存在某种方法或公式可以让你从用例或实际物体里推导出领域模型。领域模型是“分析”不出来的,它是被“设计”出来的。所以,领域模型里会有一些现实世界里并不存在的实体,当然也会有与现实世界里的物体同名的实体,但是它只是表现现实物体的某个方面,所以它的职责也就很可能与现实物体不同。至于报表,一般多是取自几个实体中的数据,还要进行汇总等统计操作,所以比较适合用来验证模型,而不是一开的创建模型的工作。
殷六侠:唉,师傅,您越说我就越糊涂,恐怕弟子是要辜负师傅的重托了……
张三丰:别急,其实建模还是有一些实用技巧的,待为师传你建模心法。呃,今天时候不早了,明天再说吧。拜拜
殷六侠:师傅晚安。
殷六侠回到客栈,周围突然一下子变黑了,屏幕上出现一行小字:“正在存盘……”
第二天的QQ聊天记录
殷六侠:师傅早。
张三丰:早。昨天说到哪了?对,建模心法。先传你建模心法1。
建模心法1 寻找线索实体。
殷六侠:什么叫线索实体?
张三丰:就是生命周期恰好贯穿整个业务流程的那个实体。这个实体就像一条线,将整个业务流程中的其它实体串起来,形成星型的结构。
殷六侠:我明白了,例如一个企业的销售业务就是签订合同、执行合同,那么“合同”就是这样的一个线索实体。
张三丰:没错。
殷六侠:让我想想,对于住院管理来说,这个线索实体的生命周期应该在患者入院时开始,患者出院时结束。“病历本”符合这一条件,不过让其它的实体都关联“病历本”似乎不大自然。由于患者可能多次住院,所以患者这个实体的生命周期显然过长而不适合作为线索实体。
张三丰:很好。
殷六侠:这个线索实体可以定义为“患者的一次住院”。嗯……可以叫“住院履历”,或者干脆叫“住院记录”好了。
张三丰:这个名字还算凑合吧。起一个好名字还是很难的,以后有空多去武当山下的酿名斋坐坐。
殷六侠:是,师傅。有了这个线索实体类,其它的几个相关的实体也很自然地产生了。
张三丰:看上去挺不错的,不过我要提醒你,领域模型是要能满足所有用例的所有场景的,这个模型里没有包括费用相关的实体呀。
殷六侠:是啊,直觉上费用的处理挺复杂的。
张三丰:你的直觉很正确。现在为师传你建模心法2。
建模心法2 寻找相似场景。
张三丰:如果几个用例中都包含相似的场景,例如“计费”,就可以把这些相似的场景抽取出来成为一个单独的用例,再让其它用例包含(include)这个被抽取出来的用例,不过这不是必须的。最重要的是你要认识到寻找相似场景的意义。越多的用例包含这个相似场景,就说明这个场景的业务越复杂,设计不当的可能性越高;将来重构的成本也越大。换句话说就是风险越发的高,所以更需要你加倍仔细、小心地处理。
殷六侠:您是说我们要设计一个实体-关系结构,可以满足所有的相似场景?感觉好难的说。
张三丰:没错,有时会感觉太复杂而无法把握,这时可以试试建模心法3。
建模心法3 使用示例场景(Sample Scenario),寻找一致性。
张三丰:示例场景是一组(最好是连续的)模拟真实业务的场景。例如和费用相关的示例场景为:
场景一:患者入院登记。交预缴金200元。申请获得500元担保金。为医保患者,医保卡内有1000元。
场景二:第一天消费感冒通一盒(50元)、抽血一次(30元)、床位费(100元)
场景三:经申请,担保金额度升为1000元。另,护士重新读取了医保卡,医保卡里的余额是2000元。
场景三:镶金牙(1000元,走现金帐户),担保金+账户余额=100元小于最低预缴金额(200元,系统参数),要求续费
场景四:续交现金预缴金2000元。
场景五:护士刷医保卡缴费。
最好与领域专家共同制作示例场景。要涵盖所有可能的情况,同时注意不要包含现实业务中不存在的情况以避免不必要的复杂性。如果领域专家指出某种情况是不存在的,应该进一步追问其原因。原因可能是“目前为止还从未出现过这种情况,虽然理论上是合理的”,“这种情况是违反法律或行规的”,“这么做将伤害公司或客户的利益”,“为了方便某个部门或某类员工的工作”、“也许这么做会更合算,但是我们领导偏偏就要求要按现在的做法去做”等等。无论是何种原因,都可能成为未来的变更点。虽然目前的设计不必理会这些不可能情况,但是可以思考一下将来发生变更时如何修改现有设计,以此为契机很可能会发现更为简单且富有弹性的设计。
上面的示例场景是不完全的,因为没有涉及退费相关的场景(抱歉限于篇幅没有给出退费相关的用例),就以上面的5个示例场景,该如何设计领域模型呢?
可以注意到示例场景中存在两类金额:预缴金和担保金。执行某医嘱时可能从预缴金中扣钱,但是也可能从医保卡中扣钱。出于需要为患者打印每日费用明细(抱歉限于篇幅没有给出这个用例)和退费,需要记录每笔费用,这让你想到了什么?
殷六侠:这让我想到可以试试账户+变更记录这个模式(关于常用的分析模式,我打算以后再写几篇来专门介绍),让我仔细想想……
1小时之后
殷六侠:我想到医保卡余额与担保金在概念上很相似,这样就可以一致地处理医保卡账户和现金账户了。
殷六侠:领域模型变成这样:
殷六侠:示例场景确实给了我不少感性认识。
张三丰:随着OOA&D的流行,人们普遍认同一开始建模的时候不应考虑实现细节,而应把注意力集中在对领域的深刻理解上。领域模型里的对象结构与实际的数据库表结构可以有很大的不同;实体间的关联也不一定非得实现为外键关联……不过请不要把可以忽略实现细节等同于可以忽略细节领域知识。正所谓“细节就是魔鬼”,醉心于抽象和一致性,而没有把同等甚至更多的精力投入到发现不一致的细微之处上,实在是一种很危险的做法。所谓“具而不抽则罔,抽而不具则殆”,让思维在抽象和具体之间来回移动才更有效。示例场景还给了你一个“横切”用例的机会,可以让你对业务有一个更加立体的认识。
殷六侠:制作示例场景还是一个发现遗漏用例和提出尖锐问题的好机会。例如刚刚我就很自然地想到“担保金只是对现金账户来说的么?还是对现金账户和医保账户同时有效?例如当现金账户和医保卡里面的钱都用光了,而担保金为3000元时,这时计费只能走现金账户还是也可以走医保账户呢?”
小结 USM三视图法
运用本文介绍的USM(用例-示例场景-领域模型)三视图法,可令 Modeler 从不同视角观察企业业务,让思维在抽象和具体之间来回移动,互相促进、完善,发现未曾注意的细节,找出概念上的一致性,不断加深对领域的理解。希望USM三视图法成为虚空中的一块踏脚石,帮助 Modeler 迈出建模第一步。
参考文献
Craig Larman,UML和模式应用。机械工业出版社,2004.
Martin Fowler 著,徐家福 译,UML 精粹(第2版)标准对象建模语言简明指南。清华大学出版社,2002。
Eric Evans, 领域驱动设计(影印版)。人民邮电出版社,2007。
Martin Fowler, 分析模式(影印版)。中国电力出版社,2003.
RicCC,分析模式读书笔记。博客园,2008.