做强化自测题获得“软件方法建模师”称号
《软件方法》各章合集
8.1.5 要重视分析工作流

分析,就是从核心域的视角构思系统的内部机理。
在现在的很多软件组织中,分析工作流的技能被严重忽视。很多开发人员上手就直接编码,原因并不是软件开发项目的核心域逻辑极其简单,不需要分析,或者他的大脑极其发达,在大脑里就可以完成分析,而是开发人员缺乏分析的技能,只好草草跳过这一步。
为了遮掩自己的无能,开发人员还会使用各种遮羞布——引入各种核心域逻辑之外的因素把水搅浑。
图8-9 向核心域逻辑中引入其他因素
遮羞布一:时间
以“时间紧”、“敏捷”为借口,掩盖自己没有能力剖析复杂逻辑的事实——我是有能力剖析的,但时间太紧张了,等以后有时间吧!
遮羞布二:空间
借助“口头交流”、“白板”等容量小的介质,掩盖自己没有能力剖析复杂逻辑的事实——我是有能力剖析的,但白板空间太小了,只好简单画个“草图”了!
遮羞布三:功能需求之外的其他需求或设计因素
在思考核心域逻辑时,频频提出“这样会不会速度慢”(质量需求)、“我们想把它分成N个微服务,让不同团队用各自技术栈开发”(设计约束,也有可能是臆想的设计)等和核心域逻辑无关的因素,掩盖自己没有能力剖析复杂逻辑的事实——我是有能力剖析的,但还要考虑到这个因素、那个因素,所以精力就不够了。
遮羞布四:重构
以“后面再重构”为借口,结合其他遮羞布,掩盖自己没有能力剖析复杂逻辑的事实——我先随便写写,后面再重构,哎呀,没想到啊,时间来不及了(遮羞布一)。
关于重构,此处多说两句。
上世纪80年代末,Bill Opdyke(http://laputan.org/pub/papers/opdyke-thesis.pdf)和Bill Griswold(https://cseweb.ucsd.edu/~wgg/Abstracts/gristhesis.pdf)等人归纳了一些调整代码结构的手法,称为“重构”,后经Martin Fowler等人推广而广为流传。
“重构”的知识可以看作是建模知识的一个子集。如果开发人员真的熟练掌握重构的手法,很多情况下他已经有能力直接建模系统的核心域逻辑得到更合理的结构,根本不需要先走很多弯路再回正路。
要是开发人员以“重构”为理由拒绝思考,很可能他的所谓“重构”也是空话。
摸着石头过河是难免的,但应该在不得不摸的时候才摸,不应该假装看不见已有的路和桥,无论大小事都主动追求摸着石头过河。
当然,也有的人不是假装看不见路,而是真的看不见路——就是个睁眼瞎。不过,大脑不用思考,凭感觉摸着石头过河不停刷工作量,也是一种躺平的幸福。
用考试类比
学渣参加考试时,会这样遮掩自己的无能:
遮羞布一(时间):抱怨时间紧张或者故意提前交卷——如果再给我一些时间,我肯定做得出来。
遮羞布二(空间):故意带不好写的笔或抱怨草稿纸质量差、答题的地方太小——不是我不想好好答,可惜这个纸笔不给力。
遮羞布三(其他因素):抱怨和学科知识无关的其他因素,例如要求用仿宋体答题——不是我不会,写仿宋体耗费了我很多精力。
遮羞布四(重构):我先随便答,一会回来再检查。(结合遮羞布一)哎呀,来不及检查了。(结合遮羞布二)哎呀,答卷上地方不够了。
8.1.6 分析方法学历史的简单回顾
1958年,John W. Young Jr.和Henry K. Kent发表“Abstract formulation of data processing problems”,第一次提出在独立于实现的抽象级别上定义系统的规范。
图8-10 摘自 “An abstract formulation of data processing problems”(Young JW, Kent HK,1958)
1959年,CODASYL(数据系统语言会议)成立。1962年,CODASYL提出了一个和Young/Kent类似的模型,称为“信息代数”(Information Algebra)。
1970-1980年代是结构化分析方法的时代,主要贡献者有Börje Langefors、Chris Gane、Trish Sarson、Tom DeMarco、Pin-Shan Chen、E. F. Codd等人。结构化分析的主要建模方法是数据流图和实体-关系图,这两者的结合,让软件开发人员有能力剖析大型系统。
图8-11 摘自 Structured analysis and system specification(DeMarco T,1979)
图8-12 摘自 The Entity–Relationship model: Towards a unified view of data(Chen PPS,1976)
1982年,Nastec公司开发出了DesignAid,这是第一款CASE(计算机辅助软件工程)工具。随后,其他CASE工具陆续出现。据PC Magazine的1990年1月30刊统计,当时已经有超过100家公司提供了将近200款CASE工具。
图8-13 摘自PC Magazine 1990年1月30日刊(红框圈住的内容说明了工具的数量)
1980年代后期,面向对象的思想开始用于分析和设计。然后,UML统一了表示法。这部分历史已经在本书第1章“UML简史”部分讲述,此处不再赘述。
图8-14 摘自 Object Oriented Analysis, 2nd Edition(Coad P, Yourdon E, 1990)
图8-15 摘自 Object lifecycles. Modeling the world in states(Shlaer S, Mellor SJ, 1992)
8.1.7 本书使用的分析方法
分析模型描述系统要封装的核心域知识。
用什么建模概念来思考和描述核心域知识,可以有很多种选择。例如,“人”用不同的建模概念描述,可以说它是一个“类”,也可以说它是一个“类型”、一个“实体”。
本书使用面向对象的建模概念来描述分析模型,从三个视角来描述:
分析类模型:描述系统中各个类以及类之间的关系,如果用UML的类图表示,则为分析类图。
分析状态机模型:描述某个类的各个行为的逻辑,如果用UML的状态机图表示,则为分析状态机图。
分析交互模型:描述某些类在实现某个用例时的协作,如果用UML的序列图表示,则为分析序列图。
图8-16 本书的分析方法所使用的UML图形
需要说明的是,虽然我们用的是面向对象的分析方法,也就是说,用面向对象的概念来剖析核心域知识,但不意味着你的系统一定要用特定的“面向对象”编程语言、特定的存储方式或物理分布形式来实现。
也许你使用的编程语言是面向过程语言,例如C;也许你使用的编程语言是函数式语言,例如F#;也许你使用的存储系统是关系数据库系统,例如SQL Server;也许你使用的存储系统是非关系数据库系统,例如MongoDB;也许你的系统运行在同一台机器上,也许是分布在很多台机器上……
不管你的系统的实现方式和运行形态如何,从分析过渡到设计时,变化的只是分析到设计的映射套路。如果设计所使用的非核心域比较“面向对象”,那么映射套路会比较直观一些,否则,就需要一定的转换。但无论如何,如前文所说,这个套路和具体的核心域知识没有关系,我们并不需要针对每一个核心域概念逐一花费脑力去思考它。
我们之所以选择在分析工作流使用面向对象的分析方法,是因为从思考深度和表示的严谨程度来看,面向对象的分析方法以及UML表示法目前仍然是剖析和整理核心域逻辑的最佳选择。
本书在设计工作流的内容,会展示分析模型和各种实现方式的映射套路。
8.1.8 自测题
扫码或访问http://www.umlchina.com/book/quiz08_01.html完成在线测试,做到全对以获得答案。
1[多选]
关于分析和设计的区别,以下说法不恰当的有:
A) 分析着眼于“系统做什么”,设计着眼于“系统怎么做”。
B) 分析和设计分离的好处是,先全局思考整个系统的各个类以及类之间的关系,再有规律地映射到实现的平台和语言,这样就减少了反复试错的成本。
C) 有时候,在分析工作流也会考虑内存、网络带宽等概念。
D) 分析注重业务,设计注重技术。
2[单选]
掌握MVC、MVP、MVVM、六边形、洋葱型……等模式或架构,并不能解决分析的问题,原因是:
A) 它们描述的是域之间的协作。
B) 它们没有得到广泛使用。
C) 它们没有体现面向对象的思想。
D) 它们不够敏捷。
3[单选]
第一款CASE(计算机辅助软件工程)工具是:
A) Rose
B) ERwin
C) FlowChart/360
D) DesignAid
4[多选]
本章中目前为止提到的在分析时逃避思考遮羞布有以下哪些?
A) 不使用UML的类图和状态机图。
B) 谈论性能问题。
C) 抱怨免费或便宜的建模工具太弱,要求购买更贵的建模工具。
D) 引入微服务架构以简化核心域逻辑。
5[多选]
以下说法正确的有:
A) 分析模型就是用核心域术语表达内容的模型。
B) 分析模型概要地描述核心域知识,设计模型将核心域知识细化。
C) 面向对象的分析模型不妨碍使用面向过程的设计。
D) 口头表达也可以表达分析模型。
6[单选]
有一篇文章,作者在白板上画了一个类图,然后开始掰着指头数这个类图缺什么,"没考虑到持久化","没考虑到对象的创建"……然后得出结论:画这个类图不如直接编码。根据本节的知识,以下正确的说法是:
A) 作者不了解核心域和非核心域分离的重要。
B) 别急,这个图会越来越细,逐渐添加作者认为缺少的那些东西。
C) Talk is cheap. Show me the code.
D) 敏捷是建模的精髓,加上这些就不敏捷了。
7[多选]
以下系统中,适合用面向对象的分析方法建模核心域逻辑的有:
A) 电磁轨道炮武器控制系统
B) 互联网拼单购物系统
C) PC单机角色扮演游戏
D) 电梯控制系统
8[多选]
以下系统中,适合用面向对象的分析方法建模核心域逻辑的有:
A) 用SQL存储过程来实现核心域逻辑的企业应用
B) 用C语言实现核心域逻辑的嵌入式应用
C) 用JavaScript实现核心域逻辑、读取本地文件,不需要服务器的小游戏
D) 在微软Azure上运行的企业应用
9[多选]
以下系统中,适合用面向对象的分析方法建模核心域逻辑的有:
A) 火箭发射控制系统
B) 函数式语言的编译器
C) 视频剪辑软件
D) 关系数据库管理系统
8.2 建模步骤C-1 识别类和属性
8.2.1 面向对象的假设
注意标题中提到的"假设"二字。面向对象就是一种假设,如果不认可“面向对象”的假设,也可以分析系统的核心域知识,只不过用的方法不叫“面向对象方法”,叫“面向过程”、“面向组件”、“面向肥皂”、“面向武德”都行,看你的假设是什么了。
面向对象的思考方式比目前的其他思考方式要好一点,原因不是计算机喜欢面向对象或者面向对象更接近于计算机的底层,而是面向对象的思考方式更能帮助人脑去剖析复杂问题。如果计算机有感情,估计它应该更"喜欢"人类用机器语言直接给它发指令,因为这样自己就不用受累搞什么编译、链接。
正如前文提到的,三角函数更能解决复杂问题,不意味着它比全等三角形、相似三角形更容易掌握。面向对象更能帮助剖析复杂问题,不意味着面向对象的思考方式比其他的思考方式更容易掌握,而且随着你掌握了更强有力的思考工具,更复杂的问题就会扑面而来。这些问题早已存在,只不过之前你没有能力来发现和对付它们——“古人很少死于癌症”。
当使用面向对象的方法来分析系统时,我们引入的第一个假设是:
系统由"对象"这样一种东西构成,对象封装了数据和行为。
严格来说,我们建模的是类(也许叫“基于类的方法”更合适),即对象的“模板”,而不是对象。对象是运行时(或模拟运行时)才产生的。
我们通过抽象思维把具有共同特征的对象集合归纳为"类",对象看作类的实例。归类是人类认知的一种基本技能,其哲学讨论可以追溯到柏拉图的理型论(Theory of Forms)。
我们引入的第二个假设是:
对象在一个"对象空间"中运行,在这个空间中发生的所有事情消耗的时间为零。
图8-17 想象的"对象空间"
您可以认为这个"对象空间"存在于大脑中,也可以把"对象空间"想象成一台存储空间无限大,通信和运算速度无限快且分布在全宇宙的超级计算机。在这个假设下,不用考虑什么硬盘、内存、Cache、加载,只需要聚焦于思考核心域知识。
当然,“时间为零”、“无限快”是不可能的。我们的宇宙,目前因果关系的最快速度是光速。
当前现实中的计算机和网络,要发生一段因果关系,例如,从提交关键词到返回查询结果,时间估计会以秒来计,不过,作为人类的涉众可能对此已经表示满意了。
如果当前的计算机和网络资源能够满足人们对性能的要求,那么设计模型(代码、存储……)和分析模型之间映射会非常直接。
反之,如果出现不可调和的性能问题,设计模型可能会有所调整,例如,添加一些冗余,但这样的调整和具体的核心域知识无关,可以把它们归纳成一些套路,出现相应问题时按照套路调整即可,不需要在分析时考虑这些问题。
“不考虑性能”这一点,可以用来判断你思考的问题是分析问题还是设计问题。
我们可以针对分析模型里的元素,一个一个问,“如果没有它,会怎么样”,如果回答是“会有性能问题”,那么,可以从分析模型中把它删掉。
例如,类图中有一个冗余的类,问“如果没有它,会怎么样”,答“查询可能会慢”——可以删掉。状态机图里有一个状态“Transient”,问“如果没有它,会怎么样”,答“会漏掉某些数据没有持久化”——可以删掉。
但如果回答是“没有它,系统就没法履行自己的责任了,因为要做的系统就是一个持久化框架”,那就不一样了。
分析模型受到设计的污染,很容易导致批量的废话刷工作量,导致没有时间思考应该重点思考的核心域逻辑。当然,正如前文所说,这也可能正是某些人故意寻找的遮羞布。
8.2.2 三种分析类
8.2.2.1 Ivar Jacoson的假设
我们引入的第三个假设是:
系统中存在三种分析类:边界类(Boundary Class)、控制类(Control Class)和实体类(Entity Class)。
这个假设借用了Ivar Jacoson在“Object-Oriented Software Engineering: A Use Case Driven Approach”(Jacobson 1992)中的思想。
在UML模型中,我们可以用Ivar Jacoson建议的构造型(Stereotype)来表示三种分析类,如图8-18。
图8-18 三种分析类的构造型
一些UML工具(如Enterprise Architect、Visual Paradigm)已经内置了这些分析类构造型。如果使用的建模工具没有内置这些构造型,可以自己添加如“<<边界>>”等文字构造型;或者不用构造型区分,通过给类起名"某某接口","某某控制",也有助于了解该类在系统中扮演的角色。这一点,和第3章讲到业务工人、业务实体时的做法是一样的。
在设计工作流,三种分析类可以映射到任何实现架构,包括但不限于MVC、MVP、MVVM、六边形、洋葱型……甚至映射到不做任何分割的“架构”。
图8-19列出了三种分析类的责任、和需求的关系以及命名。
图8-19 分析类的责任、和需求的关系以及命名
图8-20用序列图展示了三种分析类之间的协作。
图8-20 三种分析类在系统中的协作
执行者先把消息发给边界类对象。边界类对象履行它有能力履行的责任,然后把它没有能力履行的责任委托给控制类对象。控制类对象就像总裁办,不做具体工作,只是将责任分解后分配给实体类对象。
分配给实体类对象时,如果某个对象被其他对象组合,应该先分配给组合它的对象,再由该对象分配给它。DDD话语体系中的“聚合(Aggregate)”和这一点类似,本书在后文讲述类关系的章节中会进一步阐述其中差别以及“聚合(Aggregate)”的伪创新。
最后,由边界类对象向外系统反馈信息,完成一个交互回合。
8.2.2.2 关于边界类
边界类的责任是接受输入、提供输出以及做简单的过滤。
图8-19中提到边界类的映射方法——每个有接口的外系统映射一个边界类。这里说的"有接口的外系统"不仅包括系统执行者,还包括仅接受系统输出信息的外系统。
以2018版《软件方法(上)》UMLChina系统案例中的"时间→发送公开课通知"用例为例。该用例进行过程中,系统会向软件开发人员发送公开课通知,同时还要向UMLChina助理反馈发送通知的进展。软件开发人员和UMLChina助理在这个用例中仅仅是接受输出,没有输入信息给系统,但系统可以分别设置一个边界类来封装向软件开发人员和UMLChina助理反馈信息的责任,如图8-21所示。
图8-21 外系统映射边界类
图8-21中,“时间”映射一个“时间接口”,“软件开发人员”映射一个“软件开发人员接口”,“助理”映射一个“助理接口”。
外系统如果是人,对应的边界类也可以叫“界面”,例如“助理界面”。本书就不区分了,一律起名“接口”。
分析工作流的边界类不暗示任何实现方案。在总责任相等的前提下,它和实现的映射是多样的,可以是图形、文本、语音、远程调用……。
即使使用图形界面实现,也不能简单认为一个边界类对应一个窗体(Form)。一个边界类的责任可以拆解到多个窗体上,一个窗体也可以和多个外系统交互。如何组织这些责任,应该从外系统的角度来考虑,而不是从用例或实体类的角度来考虑。
图8-22中,“助理接口”边界类被圈住的几个责任来自不同用例的步骤,但在使用图形界面实现时,可以放在面向助理的、通知专用的同一个窗体中。
图8-22 边界类责任的组织
类似的例子还有:一份申请,需要通过系统审批三次,也就是三个不同的用例。在图形界面实现中,可能不需要准备三个窗体,部门主管、财务、副总三个审批人可以在同一窗体上工作,但部门主管、财务、副总各自有对应的分析边界类。
如果某个外系统和系统的交互很多,对应边界类的责任可能会有很多。有的做法推荐按"外系统+用例"的组合映射边界类,这样可以减少一个边界类上的操作个数。本书不推荐这样做,因为这已经隐含着先入为主“按用例划分边界”的意思,不利于最后得到合理的边界。
尽量保持一个外系统映射一个分析边界类,如果操作很多,可以将从外系统角度观察可能要分在一组的操作移到一起,EA等工具可以随意定制属性和操作的上下显示顺序。
需要提醒的是,外系统映射的只是边界类,并不映射实体类。在外系统是人的时候,经常会有人犯这样的错误。例如以下用例规约片段:
1. 助理选择公开课,请求创建通知任务
2. 系统验证所选公开课适合创建通知任务
“助理”是执行者,映射一个边界类“助理接口”是可以的,但如果映射一个“助理”类,如图8-23,那就错了。
图8-23 外系统不映射实体类
系统是否需要一个“助理”类,要看系统是否需要维护助理的信息。如果需要,会在某个用例规约的某个地方体现,例如,可能会有一个步骤:
7. 系统保存通知任务
绑定一个字段列表:
7. 通知任务=4+创建时间+创建人
这个“创建人”就是助理,说明系统需要记住助理的信息,这时才会有“助理”类。
但并不是所有的系统都需要保留人的信息。例如,乘客坐电梯上楼,乘客是电梯系统的执行者,但电梯系统可能不需要"乘客"实体类,因为它不需要记住乘客的信息。
当然,有朝一日,电梯升级为防疫电梯,用例规约里有:
4 乘客提供身份标识
5 系统验证身份标识合法
6 系统记录乘客信息和入厢时间
这时,电梯系统里就有"乘客"实体类了,因为系统要记住乘客的信息。
当然,虽然电梯系统没有"乘客"类,但会有"乘客接口"类,可能的类图和常见的实现方式如图8-24。
图8-24 “乘客接口”及其常见的实现
8.2.2.3 关于控制类
控制类相当于用例在系统中的“代理”,它的责任是控制用例流,为实体类分配责任。如果在分配责任时发现控制类只起到传递的作用,没有起到分解和分配的作用,也可以把控制类去掉。
因为每个用例直接映射一个控制类,可以用“用例名称+控制”来为控制类命名。
因为构造型已经包含“边界类”、“控制类”的概念,严格来说,“员工接口”、“审批控制”类名后面的“接口”、“控制”可以省掉,在分析映射设计时,再通过映射套路中把它补上去。不过,考虑到可能还会有“员工”、“审批”实体类,而且很可能会一起出现在同一张序列图上,仍然保留“接口”、“控制”的尾巴看起来更顺眼一些。
8.2.2.4 关于实体类
边界类与外系统、控制类与用例的映射关系很明显,所以识别边界类和控制类不需要思考,直接按照上面的套路映射即可,甚至可以先不映射,推迟到画分析序列图时再加上去。
有的分析方法学如ICONIX提倡一种Robustness Diagram,认为可以通过它来帮助寻找类。开发人员一用确实感觉很舒服,噼里啪啦就发现好多类,有一种"我已经取得了不小成绩"的错觉,不过要是仔细看看,就知道"发现"的大多是边界类、控制类。这些类其实用不着刻意去发现,只要按照图8-19的套路映射即可。
最难的工作——寻找实体类以及它们之间的协作,Robustness Diagram却是寥寥带过,甚至容易误导建模人员把实体类和用例一一对应。所以,本书不推荐开发人员额外花时间画Robustness Diagram。
★和前文多次提到的一样,凡是不需要思考就可以得到很多“成果”的“方法”,都容易成为 建模人员的思考工作量应该花在识别实体类上。一个用例需要哪些实体类协作实现、如何协作,一个实体类会参与哪些用例的实现,这是一个多对多的映射,需要由建模人员的大脑决定哪种映射最好。 因此,本章以下内容提到的“类”,缺省意思为“实体类”。 边界类、控制类的命名有“接口”、“控制”的尾巴,实体类的命名就不要尾巴了,直接用核心域概念命名即可。例如,命名为“员工”而不是“员工实体”。 8.2.2.5 不存在“系统”这个类 "系统"的概念是需求工作流的概念。在需求工作流,我们把系统看作一个对外提供服务的整体。在分析工作流,"系统"的概念已经被打碎成很多个类,"系统"这个词不需要识别成类。 图8-25表达了不同工作流视角下的目标系统。 图8-25 各工作流如何称呼目标系统 在业务建模工作流,研究范围是组织,而组织中有很多系统,在业务序列图上提到目标系统时,只是说“系统”二字无法让人理解指的是哪一个系统,需要写出目标系统的名字“系统”。 在需求工作流,研究范围是目标系统整体,此时,聚光灯已经打在目标系统身上,不需要再写目标系统的名字,写“系统”二字即可。 就像公司开表彰会,老总宣布优秀员工名单时,要说名字“罗永昊”、“罗阵宇”,轮到优秀员工罗永昊上台发言时,罗永昊称呼自己就不好再说“罗永昊”,说“我”、“在下”、“小弟”就行了。 ★用自己的名字以及第三人称来称呼自己,往往代表一种极度的“自信”。例如: “凯撒注意到这事,把他的军队撤到最近的一座山上去”(《高卢战记》) “老胡觉得这件事实在不应该” “婷婷想吃冰淇淋嘛”★ 而在分析工作流,研究范围是系统的内部构成。系统已经被分解成很多个类,这时就不能再说“系统”了。 有的建模人员会把"系统"识别成一个类,画成这样: 图8-26 无意义的类图 这种图画出来既没有根据(你怎么知道是这样分解?)又含义模糊(模块指的是需求包还是组件?),对剖析系统的复杂性没有帮助,但给建模人员带来一种虚假的成就感:我描述了几个类之间的关系,而且还是组合关联(还可以联想到高大上的“组合优于继承”),已经开始剖析系统的复杂性了呢,甚至还可以扯上领域驱动设计话语中的“聚合根”——最妙的是,不用思考,信手拈来! UMLChina公众号精选(20231208更新)按ABCD工作流分类