本文篇幅有些长,但是相比阅读各类书籍,然后理解和吸收,会大大节省很多时间,对于一些书中难以理解的部分做了改进,帮助更好的理解。可能阅读本文需要一些软件方法的基础知识,才能更好理解和吸收,甚至提出反馈建议。希望文本对大家有帮助,当然这需要运用好“只字不差阅读”和“只字不差理解”。
“系统性地输出式学习”,最大的受益者是自己,其次是“只字不差阅读”和“只字不差理解”的你。为什么:“利润 = 需求 - 设计”
- 先说需求:一种是“假需求”,一种是“有需求”。假需求做得多,企业成本增加。有需求并非一定是核心价值,通过软件方法的分析思路和工具从“有需求”中找到“真需求”,提升软件好卖带来营收增长(价值需求带来营收)。
- 再说设计:指的是用一系列软件方法分析业务流程和规则,产出需求分析,做好架构设计,提升软件系统的维护性和扩展性,进而提升软件生命周期各个环节的效率和质量,降低成本(好的设计降低成本)。
1.背景是什么?
不知道你有没有发现,当下软件行业的工程师在业务抽象与抽象、领域建模、架构设计等能力上似乎并没有因为业务快速发展,及技术框架、中间件、分布式等技术发展成熟,带来相关的能力的积累和沉淀,反而在退步或消失,甚至很多参加工作几年的同学都未曾使用过软件方法中的一些方法。
而软件方法随着企业规模变大,重要性其实应该被逐步提升才对,但实际恰恰相反,越来越不重视,而且需求越来越多,设计越来越丢失,系统扩展性和维护性越来越差,用户产品越来越复杂。
记得刚参加工作头几年,项目一旦立项,架构设计会作为重要且紧急的关键里程碑工作,经历几轮评审和修改才进入开发,而且在旁边听前辈讨论和评审也是受益匪浅,浓浓的技术氛围推着自己也去学习。但是随着互联网快速发展及敏捷迭代的流行,人们渐渐忽视了软件方法(敏捷迭代并未要求忽视设计,而是在执行时因为人的原因不再重视,慢慢地不再谈及软件方法)。而且,平时经常听到重构系统,但是每年都在重构,这是进步,还是退步,我认为是退步,这值得我们反思。
能力的退步、氛围的缺失和无效的重构,是总结本文的背景,希望在不久的将来越来越多的工程师熟练掌握软件方法中的相关方法和工具。
2.什么是软件方法?
来自百度百科的定义,软件方法是指在软件开发过程中,从软件需求分析、软件设计、软件编码、软件维护等环节中,采用适合的方法解决软件开发中的问题,实现高效的开发,以满足用户的需求。
我自己对“软件方法”理解,从业务到软件开发的一系列分析方法和工具,帮助我们将复杂的业务知识、架构知识转变为团队中人人能够理解和上手的统一语言,并借助诸如UML工具产出具体的设计图。从而更加系统化、规范化地进行软件开发,确保软件系统的可维护性、可靠性和可扩展性。
但是,软件方法长成啥样?
比如你学会了设计原则和设计模式,可以说你掌握了设计方法;比如你学会了为业务构建领域模型,可以说你学会了领域建模方法;比如你学会了通过业务用例和业务时序定义组织提供的价值和组织内如何协同,可以说你掌握了业务建模方法。
设计方法、领域建模方法、业务建模方法等方法的融会贯通,组成一个完整的软件方法。大部分人可以较好掌握设计方法,但是掌握业务建模和领域建模方法的人比较少。
3.为什么软件方法被忽视?
因为软件方法中最核心的能力,比如业务建模和领域建模能力,看起来对完成代码开发好像没有帮助,同时掌握它又有一定的挑战,导致没有得到足够多的人重视。当然还有另外2个原因:
- 只重技术不重业务,只是用代码实现业务系统,并未考虑业务扩展性、维护性等。
- 业务和领域建模是种手艺,但凡手艺都需要在实践中不断经历磨炼。
4.软件方法的重要性?
一个好的软件产品,离不开对业务的理解和抽象,作为技术人员,先要对业务充分理解,并将业务知识做抽象,才能将业务理解和抽象转换成更好的软件设计,这是技术人员能力发展非常重要的步骤及好的软件系统的前置条件。
而且软件方法技能的熟练度提升,会带来团队和个人整体交付质量和效率的提高,个人的价值和影响力也会越来越好,职业发展路径越清晰。
本文篇幅有些长,但是相比阅读各类书籍,然后理解和吸收,会大大节省很多时间,况且通过自己的总结输出,对于一些书中难以理解的部分做了改进,帮助更好的理解。可能阅读本文需要一些软件方法的基础知识,才能更好理解和吸收,甚至提出反馈建议。
最后,希望文本对大家有帮助,当然,若想真的有帮助,需要运用好“只字不差阅读”和“只字不差理解”。若总结中有错误或不合理的地方,欢迎留评指出,不胜感激。
一、软件方法概要
产品和研发在进入具体开发之前,我认为需要思考清楚如下3个问题:(1)首先,需要了解需求会不会变化;(2)其次,无论需求变与不变,要知道提升“好卖”和“降本”的方法;(3)最后,实践建模方法挖掘被研究组织提供的价值,降低实现成本。
1.需求会不会变化
人类或组织相关需求从过去到现代大部分不会发生变化,在将来大概率也不会变化,比如存/取钱、吃饭、看病、出行、看戏等。但是随着技术发展和资源逐渐充裕,很多需求会不断出现或发生演进,比如买东西不要出门、看戏到看电影/短视频、吃饭点菜时希望不要等待。无论需求变与不变,实现需求的方式会随着技术发展而逐步变化,这是毋庸置疑的。
比如,以“取钱买商品”需求为例,这个需求从古代到现在都没有发生变化,只是需求的实现方式变得更高效了。10几年前,我们要从家里先坐车去银行取完钱,然后坐车去商场买东西,最后从商场拿着商品坐车回家,一天下来将人肉这个大的物流不断挪动,人累效率又低。现在可以直接在淘宝上买商品,然后网上支付,最后由物流公司把货物寄到你家里,人肉这个物流可以做到足不出户。
而且,随着技术发展,原先因为资源和技术受限,很多需求没法得以实现,或者需求体现方式单一。采用建模思维,可以更高效和高质量挖掘出需求的价值,进而通过付出心血和努力获得竞争优势和成本降低。
比如,以“餐馆点菜”需求为例,从古代到移动互联网之前,你每次去餐馆吃饭,服务员会给你拿一张菜单过来,然后你边点他边写,遇到人多的时候,服务员根本来不及招呼你,你要过好久才能完成点菜,上菜就更慢了。但是随着互联网普及及智能手机的出现,基于二维码扫码点餐大大提升了餐馆效率及降低了人力成本,也提升食客的消费体验。
2.提升“好卖”和“降本”的建模方法
任何一家企业都不得不重视的公式:“利润 = 收入 - 成本”。作为软件行业的产品和技术人员来说,我们演进下公式:“利润 = 需求 - 设计”。通过挖掘需求价值提升产品好卖,做好软件工程的设计降低成本,进而提升利润。
建模技能的有效运用会决定需求的价值、决定软件工程设计的质量。但是,什么是建模?
建模的本质:总结经验的过程,将经验总结为方法和工具。
以人的大脑为例,通过学习、做项目积累经验、听分享、做交流等动作完成大量的信息输入之后,大脑会完成一轮建模,并总结出一些方法和模型,变成大脑的模型之一,模型越多个人能力越强,然后在下次自己遇到类似问题时,能够基于模型给出更好的方法。
放在软件开发上,建模是软件生命周期各个阶段的模型组合,包括业务建模、需求分析、领域建模和软件设计。每一个建模技能细节的提升,能够更好挖掘到产品价值或降低研发成本,提升产品“卖相”,及提升架构的质量属性(如扩展性、可维护性、安全性等),这些都会带来“利润”提升。
3.实践建模方法挖掘价值和降低成本
以“人”系统为例,人会走路,吃饭,说话,跳跃,还会拉马车、搬砖、讲笑话等,但是我们人这套系统并没有走路子系统、吃饭子系统、说话子系统、跳跃子系统。反而是呼吸子系统、消化子系统、神经子系统、血液循环子系统。人体的每个子系统互相协同完成走路、吃饭、说话、跳跃等外在功能表现,并为其他组织提供价值,比如为房地产提供搬砖价值,为小朋友讲三国演义(凯叔讲故事)。
从上面的例子我们可以学习到,实现产品不仅仅是简单的把外在功能表现当中需求,然后变成一个个子系统。合理的方式是:研究组织对外提供的价值、为了实现组织对外提供的价值,组织内需要开发什么系统、不同的系统如何协作及用什么样的技术实现更好的产品体验和性能表现。
以下以银行为例,按顺序践行建模的4个方法,因为无银行经验,银行系统的实际情况肯定和下图有出入。
通过表格进一步理解上图中的业务建模、需求分析、领域建模和软件设计的职责。
按照业务建模、需求分析、领域建模和软件设计对组织进行建模分析,可以强迫我们高质量思考,产出高质量的设计降低开发成本和维护成本,进而产出“更好卖”的产品。
二、业务建模
业务建模核心是找到“被研究的组织”向“其他组织”提供的价值,一般称其他组织为用户/客户,但用户/客户过于泛化。当然,在面向社媒/股东等,可以说我们有多少用户,但是产品和技术在讨论具体的需求细节时,需要从泛化到具体。
如何从泛化到具体,核心是用老大和涉众来替代其他组织。老大是为软件付费的人(或是最核心的用户),涉众是除老大之外相关的人,或者老大就是涉众里最重要的人。软件开发时优先满足老大需求,然后是满足涉众需求。
比如银行的涉众是家里有储蓄的人和有贷款需求的企业,银行通过一定的利息吸纳涉众的储蓄,然后以更高的利息贷款给企业,这就是银行的商业模式,各种涉众的需求都得到了满足。
业务建模采用最核心的2个模型,分别是业务用例和业务时序。
业务用例表示被研究组织对外提供的价值,业务时序表示被研究组织内部各个系统如何相互协作实现组织对外价值的提供。
还是以“软件方法-概要”中的银行为研究对象(银行每个人都用过,以它为例更容易理解),来更详细描述银行业务用例和业务时序。
1.业务用例
业务用例要素:业务执行者和业务用例。
业务用例画法:用照相机对准被研究的组织,谁和这个组织有交互,谁就是业务执行者。如下示例,拿着照相机在组织边界处拍照(比如大门),谁和组织有交互,谁“可能”是业务执行者。为什么说可能呢?原因有些和组织有交互并不一定是业务执行者,比如来问路的人,乘凉的人,在组织内工作的人等。
以银行为例,储户来银行存款,企业来银行贷款,储户和企业都是银行这个组织的业务执行者。但是银行里的职工虽然也来银行,但他们只是银行这个组织里的业务工人,并不是业务执行者。但是,如果研究的组织是银行里的某个子系统,比如点钞机,这个点钞机的业务执行者可能是银行职员。
如下图,围绕钱流通的商业模式,古代和现代的需求没有发生变化,都有存款,取款,转账和贷款需求。变化的地方在于名字不同,古时叫钱庄和商人,现在叫银行和企业。
但是,随着技术发展和资源不断涌现,组织的价值在不断演进,比如现在银行相比古代的钱庄,除了基本存取款之外,还可以为企业和个人提供诸如理财、外汇、信用卡、期权等银行组织对外提供的价值。
2.业务时序
业务时序画法:业务时序中包括业务工人和业务实体,通过业务工人和业务时序的相互协同合作,完成被研究组织对外价值的提供。业务工人(圆圈中有个小人)和业务实体(圆圈+下划线)表示方式见下图。要特别注意,业务实体的抽象级别要一致,比如系统和表结构都出现在业务实体中,肯定出现了抽象级别不一致的情况。
以“银行取款业务用例”为例,先画出银行早期的业务时序图,然后画“点钞机"出现的改进业务时序图、最后画"ATM取款机"出现的改进业务时序图。通过业务时序的不断修改(但是业务用例一直没有改变,还是银行组织对外提供取款价值),来感受业务时序在改善组织问题、提升用户价值体验研究上的价值。
2.1 柜台取款
小时候家境不富裕,有点钱妈妈会把钱拿到信用社存款赚点利息,信用社的柜员拿到钱之后要反复数几遍,有时候会拿出一张钞票对着灯光反复瞧几遍,辨别是否真假。同时家里需要用钱的时候,妈妈又会去信用社取款,当柜员把钱给到妈妈的时候,妈妈要反复数几遍,而且最担心是怕有假钞。
那个时候消费没现在这么方便,存/取款频率也没现在这么高,效率并非那时候的核心问题,反而钞票的数量和真假是那时的核心问题。
2.2 点钞机出现
于是善于研究组织问题的人发现,钞票数量出错带来的影响:要么是储户损失,要么是银行损失。同时钞票要是辨别真假出错,银行大概率会损失更严重,因为假钞会流行。
那么,不出意外也就出意外了,有人研究出了点钞机,一次性解决当时核心问题的:钞票数量和钞票真假,储户和银行再也不用担心了,银行的价值和信任感被加强。于是业务时序图有了如下变化。
2.3 ATM取款机出现
随着银行卡的普及和软件技术的发展,基于存折存/取款逐渐淡出了历史舞台,上图中的“1:服务台填写取款单”和“2.5打印存折上的取款金额”在今天已经逐渐消失,取而代之的是基于银行卡的存取款操作。
同时,随着互联网的发展,人们的消费频率越来越高,存取款的频率也越来越高,意味着银行排队的人越来越多,于是善于研究组织问题的人发现,能不能通过终端解决银行柜台不够的问题,进而为储户带来更好的体验。甚至还可以将终端安装在离储户近的地方,不用来银行就可以完成取款或存款。于是业务序列图又有了变化。
银行组织的“取款用例”虽然经历了3个阶段的业务时序变化,但是对储户提供的价值没有发生变化,而且储户的体验越来越好,社会效率也越来越高。如果某家银行没有跟上业务时序中描述的变化,业务萎缩不会意外。
截止这里,可以看出业务建模中的业务用例和业务时序是一种工具,帮助我们找准组织对外提供的价值,而且价值非常稳定不会轻易发生变化,同时帮助我们找准组织内部需要改善的问题和流程,通过数字化、人工智能、终端等方法进行改善,为用户(老大和涉众)创造更好的价值,持续不断带来用户体验的变化。
三、需求分析
在“2.3 ATM取款机出现”章节中提到,因为消费频次变高,来银行排队存/取款的人越来越多,用户存/取款体验越来越差,所以研究组织在存/取款的体验时,提出了存/取款终端设备:“ATM取款机”,进而改进了业务时序图。所以我们需求的研究范围来自“2.3 ATM全款机出现”章节改进后的业务时序图。
需求分析分为系统用例和系统用例规约。以下以ATM取款机为研究对象,分析系统用例和系统用例规约。
1.系统用例
系统用例包括系统执行者和系统,系统执行者是系统的使用者,比如ATM取款机是系统,储户是系统执行者。
先理解系统执行者和系统:
- 系统必须能够独立对外提供服务,可以是数据服务,也可以是行为服务,比如“2.3 ATM取款机出现”中的风控系统独立对外提供取款风控安全监测,短信系统独立对外提供短信通知服务。
- 系统边界是责任边界,比如“2.3 ATM取款机出现”中的风控系统、短信系统的责任完全不一样,分别提供自己职责范围内的服务。
- 系统执行者和系统要有交互,比如研究的系统是售票系统,系统执行者是售票员。如果研究的系统是12316 APP,系统执行者就不是售票员了,而是旅客。
- 系统执行者有主执行者和辅执行者,辅执行者是被动参与,比如“2.3 ATM取款机出现”章节中的储蓄系统是ATM取款机的辅执行者。
- 系统执行者不一定是人,也可以是系统或时间,比如定时器可以是系统执行者,本质上是驱动系统运行。
识别系统执行者和系统用例方法比较简单,如果业务时序图画的非常标准和详细,系统执行者和系统用例都可以来自业务时序图中。以“2.3 ATM取款机出现”的业务时序图为例,给出ATM取款机的系统用例。
因为“2.3 ATM取款机出现”只是画了取款业务时序,实际还可能包括查询余额、转账、存款等业务时序流程。所以完整的ATM取款机系统用例如下:
4个用例的主执行者都是储户,但是辅执行者没有短信系统和风控系统,原因是研究的系统是ATM取款机。如果研究的系统是储蓄系统,则储蓄系统取款用例的辅执行者会有短信系统。需要把研究的系统作为前提。
2.系统用例规约
一个用例一份用例规约,每份用例规约是需求细节的详细描述,包括用例名称、系统执行者、前置条件、后置条件、涉众利益、主要流程、扩展流程、业务规则、质量要求、设计约束等。需求描述的详细程度直接影响交付质量,用户体验。
以ATM取款用例为例:
受限于银行业务知识不足,以上的需求用例规约不一定详尽,但是从前置条件、后置条件、涉众利益、主要流程、扩展流程、业务规则、质量要求、设计约束等角度描述出来的用例,会影响从需求→研发→交付上线整个生命周期的效率和质量。
四、领域建模
DDD,中文叫领域驱动设计,领域可以理解为业务,比如邮件业务、银行存款业务等都是业务领域,告诉我们要从业务出发去设计我们的系统,是“软件方法学”的范畴,提供了一套思维模式和分析方法用于开发复杂软件的系统化方法和思想。
DDD价值是什么?可以提供系统从0到1搭建、也可以指导系统架构治理、还可以指导架构师培养等。如何运用DDD,主要是学习DDD的“手艺”方法,比如事件风暴、聚合、值对象等。
1.事件风暴识别领域名词
领域建模最核心也是最重要的一步是识别领域对象,只有领域对象被识别出来,才能基于领域对象画出领域对象之间的领域关系。如何识别领域对象有3种方法,分别是:(1)仅通过聊天就能识别,这种人是绝对高手;(2)事件风暴法,通过多人协作完成;(3)通过系统用例找对象。
为了增强协同,往往推荐事件风暴法,但需要业务、架构师、开发人员等一起,基于脑暴的方式来完成,但如果团队中,或者产/技之间没有形成这种能力,我建议由懂的人来完成(比如架构师),基于完成后的领域模型拉上相关同学分享和评审,请大家帮忙补充是否有遗漏的领域对象,然后基于新的领域对象再次完善领域关系。
事件风暴法分为3个顺序动作:识别领域事件 → 识别命令 → 识别领域名词。还是以银行为例。
识别领域事件
领域事件是指已经完成或发生的事实,比如资金已转账、资金已存款等,是完成时 + 被动语态,比如资金已存款 = 资金被存款,“已”表示完成。请记住,在识别领域事件的过程中,需要重点关注那些可能在需求文档中没有提到的领域事件及业务规则,并把业务规则找出来进行管理。
以银行为例,包括现金、理财、贷款、信用卡、积分、短信等业务。按照“完成时 + 被动语态”来识别领域事件,包括资金已存款、资金已转账、贷款已到账、资金已取款、积分已消费、资金已存定期、资金已转活期等。
其中资金已存款和资金已存定期为什么不用一个领域事件:资金已存款。是因为资金已存款和资金已转定期背后的业务逻辑不一样。比如:活期存款利息0.3%,但是一年定期利息1.75%,二年定期利息2.25%,而且活期可以随时取款/转账,定期需要先转成活期才能取款/转账,而且一旦转成活期,利息只能按照活期0.3%计算。这些业务规则需要被单独整理和管理起来。
下图以现金业务和贷款业务识别对应的领域事件及业务规则。
识别命令
领域事件是指已经完成或发生的事实,而命令是引发领域事件发生的操作,及谁执行了该命令(执行者是谁),执行该命令时做了什么查询操作(执行者→发起命令→事件发生)。
比如领域事件“资金已转账”的命令是“转账资金”,转账人是“银行柜台”,转账人在转账资金时需要查询出“目标储户”,也可以将“目标储户”理解为被执行者。有时一个领域事件的操作人有多个,比如除了“银行柜台”,可以是柜台经理,也可以是大堂经理在自助终端帮助转账,甚至是储户自己,因为储户可以在手机银行转账。
以此类推,直到将所有领域事件的命令和执行者找出来,以“现金业务”为例,识别命令和执行者后,得到下图:
识别领域名词
识别领域名词是指从领域事件、命令、执行者、查询对象(如存入资金之前需要登录储户账户)等上找出名词。比如领域事件“资金已转账”的命令是“转账资金”,命中中的资金也是领域名词,同时执行者中的“储户”、“柜员”、“柜员上级”、“目标储户”也是领域名词,
这里要说清楚:领域名词 ≠ 领域对象。领域对象可能是多个领域名词的合并,也可能领域名词是领域对象的一个角色,比如目标储户只是储户(领域对象)的一个角色,储户和目标储户合并为是储户(领域对象)。
如下图,以“现金业务”为例识别出来的领域名词有储户、资金、账户、目标账户、柜员、柜员经理、大堂经理等,可能还有不全,需要通过在后续领域模型设计,及评审过程中发现和挖掘。当然这些领域名词还不是最终的领域对象,需要对领域名称进行合并归类、抽象,比如柜员、柜员经理、大堂经理可以抽象成银行职员。
上面通过便签纸产出领域事件→命令→领域名词的方式适合多人协同,如果是架构师一个人的工作,可以通过表格的方式来整理,更简单和直观,效率也更高,比如下图:
2.理解类图6种UML关系
插入UML关系理解会有点突兀,但是先深入理解类图中的UML关系,会在接下来的领域建模中不会受限于UML概念模糊,进而影响表达领域对象之间的关系。
理解类图的6种UML关系,在后续的领域模型、模块设计、类图等关系中都会用到,但是并不是说每种设计都需要完全用到6种关系,比如模块设计可能更体现依赖关系,领域模型更多会使用关联、泛化和聚合(其实应该是组合)关系,类图中更多使用关联、泛化和实现。
加个自己的理解:依赖、组合、聚合、泛化和实现5种UML关系都是关联关系的扩展,目的是为了让业务知识或代码设计时含义更具体,抽象更清晰。比如组合表达的是整体和部分的关联关系,且部分不能脱离整体存在。
关联
表示两个类之间有联系(2端没有箭头,可以理解为是一种双向关系,比如通过客户可以以查找到该客户的账号,通过账号可以查找到账户所属客户),其中一个类对象可以访问另一个类对象的属性或方法,可以说是数据导航关系。关联关系用一条普通的实线表示。比如客户和账户之间的关联关系,客户可以查询自己账户中的资金。
具体的含义解读:1个客户至少有1个账户(如果在银行未开号就不属于客户),最多*个账号。但是一个账户只能属于一个客户,不能属于多个账户,否则账户中的资金就变成了共享资金了。
依赖
表示一个类的实现依赖于另一个类,即一个类的方法中使用了另一个类的对象,表达的范围更广。而且,关联是依赖的一种关系。依赖关系用一条带箭头的虚线表示(只有一端有箭头,是一种单向关系)。比如柜员(银行职员)接收客户转账需求时,依赖客户账号授权(提供银行卡,输入密码)才能帮助客户完成转账。
依赖常见应用场景,可能更多出现在模块的依赖关系,比如根据依赖倒置原则,低层模块不能依赖高层模块,比如应用层依赖领域层,但是不能出现领域层依赖应用层。
组合
表示整体与部分之间的关系,整体包含部分对象,部分对象不能脱离整体而独立存在。组合关系用一条带实心菱形箭头的连线表示,指向整体类。比如人和四肢,没有人,四肢也没有存在价值;比如员工和员工技能,没有员工,员工技能也没有价值,所以组合的另外一种隐藏价值是用于保护业务规则被破坏的一种手段,是领域模型中非常重要的一种关系。
什么是规则被破坏,比如在并发场景下,(1)线程A查询员工小明编程技能,没有编程技能;(2)线程B也同时查询员工小明编程技能,也没有编程技能;(3)线程A添加小明编程技术,添加成功;(4)线程B也开始添加小明编程技能,因为之前没有查询到,也会新增编程技能。这样导致的结果是,小明的编程技能被增加了2条,但业务规则要求编程技能只出现一条。
合理的方式,是把小明的信息作为整体,不能被2个人同时操作,也就是说把小明作为整体锁起来,只有小明事务整体提交,并释放锁之后,其他线程才允许操作。
比如,银行系统中,现金账户和定期账户、活期账户是整体和部分的关系,现金账户中的资金分别活期、定期。
聚合
表示整体与部分之间的关系,整体可以包含多个部分对象。聚合关系用一条带空心菱形箭头的连线表示,指向整体类。比如,人和汽车、电脑的关系,汽车和电脑是我的财产,我也可以转移财产给家人用,甚至卖掉(组合中的人和四肢关系,四肢没法转移,甚至卖掉)。
比如,银行职员的业务结果包含哪些?可以用聚合表示,比如包括个人客户和企业客户,因为如果职员离职了,个人和企业客户都可以转交个接手的银行职员跟进。
领域模型经常提到聚合,但却用实心菱形箭头连线表示,可以说表达的是组合,关系却用了聚合关系表达。
泛化
泛化用空心箭头表示,是一种统称或分类关系,比如生物可以分为动物和植物,动物又可以分为哺乳动物和爬行动物。或者哺乳动物和爬行动物统称为动物。也就是说,泛化出来的对象是一种更抽象的概念,能够表达不同对象间的共性和个性。
比如,哺乳动物和爬行动物共性部分是都有呼吸系统,用于呼吸氧气和排出二氧化碳;个性部分在于哺乳动物的卵类很少,多数是胎生的,而爬行动物的卵类很多,多数是卵生的。
比如,银行系统中的账户和贷款账户,资金账户可以用泛化表示,共性部分是都需要有账号和密码验证,个性部分在于贷款账户是欠钱(贷款金额)、资金账户是储蓄(储蓄金额)。
实现
表示一个类实现了一个接口,必须实现接口中定义的所有方法。实现关系用一个空心的带箭头的虚线表示。实现关系一般出现在代码设计的类图上,是一种面向接口编程思路。比如,电商购物支持买家在线付款,付款类型包括招商银行、工商银行、花呗等。
备注:上面的UML图中,有些有+,有些没有,区别是什么?
1.代码UML图:+在UML图中表示访问权限,比如+表示public,-表示private,没有表示包访问权限。
2.领域模型UML图:领域模型表达时没有必要体现代码实现时的访问权限。
3.建立领域模型
事件风暴的价值是识别出领域名词和业务规则,是对业务的直接描述,而领域建模的价值是将领域名词转为领域模型,需要抽象和提炼,比如把柜员,大堂经理抽象成岗位,更加深入业务本质,而不仅仅停留在表面,是DDD最重要的成果沉淀。建立领域模型包括从一堆领域名词中识别领域对象,梳理领域对象之间的关系,领域对象的关键属性,将领域对象组成模块等。产出领域模型有2个好处:
1.将领域知识可视化,方便日常沟通、共识。
2.指导软件设计和编码,转换成代码和数据库。
领域模型通常用UML来表达,UML中文名称是“统一建模语言”,从名字可以看出2个关键信息:“统一”和“建模”。“统一”是指统一了大家沟通时的语言和名词,沟通效率高;“建模”是指方法,采用UML工具可以帮助我们完成建模的动作。
UML中最重要的概念包括:类和类6种关系(聚合、组合、泛化、实现、关联、依赖),类之间通过6种关系连接产生的图,叫类图。
领域模型中最重要的概念是实体和值对象,领域模型中的实体可以映射为类,画领域模型就是画实体间的类图关系,比如账户是实体。
画领域模型的时候,一开始只需要画领域对象的关系,且领域对象一般用中文表示(避免画的时候就考虑到我的实体就是类名),同时领域对象的属性也可以先不体现,或者只体现核心属性帮助理解领域对象,毕竟我们的目的是通过领域模型表达业务知识。
在画领域模型图的时候,需要区分领域模型是描述业务,类图是技术视角的视线,比如下图:
解读领域关系图
储户 vs 客户:事件风暴中识别出来的领域名词是储户,这里为什么把储户改成了客户?主要是对储户做了一层概念抽象,字面理解储户一般是存钱,但是去银行不仅仅是存取钱,还包括贷款,理财等,可以是个人,也可以是企业,所以统一用客户作为领域对象(领域建模时需要把看到的现象进行抽象)。
新增资金流水:事件风暴中未识别出来资金流水领域名词,这里为什么加上资金流水领域对象?主要是资金会发生变动,而且资金的安全性和准确性要求非常高,需要能够追溯每笔资金动向,所以这里加上了资金流水领域对象(事件风暴方法中可能会存在潜在看不见的领域事件,或者用例中可能无法看见的用例)。
资金 vs 现金账户:事件风暴中识别出来的领域名词是资金,这里为什么把资金改成了现金账户?主要是事件风暴阶段,我们只考虑存取款等操作,更多的是对资金操作。但是客户在银行的业务可能是现金业务,也可能是贷款业务、信用卡业务、理财业务,每个业务应该是独立的业务规则和业务流程。
操作人 vs 职员&账户:资金流水关联操作人,但是操作人和储户&职员的关系是什么,表示现在还没完全想清楚用哪种UML关系表达。
新增岗位、交易类型等:事件风暴中识别出来领域名词是柜员,柜员经理和大堂经理,但是上图中领域名词变成了岗位,这也是画领域对象时经常会用到的方法,需要做一层领域名词提炼,避免直接平移。也就是说我们眼睛看到的事物,不一定和系统中完全一一映射。
关联
表示两个实体之间有联系(2端没有箭头,可以理解为是一种双向关系),比如资金账户和资金流水是一个关联关系,每发生一笔资金流水,就会产生一次关联,所以资金账户和资金流水1对多的关系。
当然可以再抽象,比如发生资金流水本质上是发生了一笔交易,也就是说可以抽象出“交易”实体,资金账户和交易产生关联关系,资金流水和交易是聚合关系。
思考:资金和资金流水为什么不用聚合关系?资金流水无法独立于资金。
聚合
按照UML关系中的聚合概念:聚合表示整体和部分关系,部分离开整体可以独立存在,比如人和汽车,虽然汽车归属我,但是我可以将汽车借给别人。同时聚合关系用一条带“空心”菱形箭头的连线表示,指向整体类。
但是在领域建模中聚合用“实心”菱形箭头表示,按照我自己的知识体现理解,用聚合表达不对,或者通过UML关系表达业务知识时,把组合和聚合统称为聚合,表达整体和部分关系,且部分离开整体不能独立存在。
比如上图,资金账户是整体,活期和定期是部分,资金账户总额由活期和定期组成,活期和定期有明确的归属账户,不能离开资金账户独立存在。如果独立存在,相当于钱跑到别人账户里去了。
泛化
泛化用空心箭头表示,是一种统称和分类关系,比如生物可以分为动物和植物,动物又可以分为哺乳动物和爬行动物。或者哺乳动物和爬行动物统称为动物。也就是说,泛化出来的对象是一种更抽象的概念,能够表达不同对象间的共性和个性。
比如上图,账户是统称,而资金账户、贷款账户、及未画出来的信用卡账户是分类。他们之间的共性在账户中,比如账户所属人,账密。他们之间的个性部分在各自内部,比如资金账户是银行付息给储户,贷款账户是储户付息给银行。同时每一个分类的业务规则和业务流程差异较大。
思考:账户和资金账户,贷款账户为什么不用聚合关系?是否可以用聚合?
值对象
实体和值对象容易模糊,但若抓住“不可变”这个规则,就比较容易区分了,比如岁数5是一个值对象,5放在我身上和放在其他人身上都一样,或者我儿子今年5岁,明年虽然6岁,5还在。
值对象可以是原子的,比如数字5。也可以是复合的,比如姓名,由姓和名组成。在领域模型中抽象值对象,主要是抽象出组合值对象,或枚举类型。比如员工状态,包括实习、正式和离职,员工状态可以被定义一个值对象。
业务规则
业务规则是软件非常重要的组成一部分,规则没有被遵守,导致出现软件Bug或漏洞。如何更好地管理规则,简单的方法是基于系统用例进行集中管理,按模块管理,比如下列表格:
五、软件设计
软件设计范围非常广,比如概要设计中的物理架构、逻辑架构、数据架构、可用性、安全、高性能等。详细设计中的接口设计、表结构、类图、状态流转等。
本文是从软件方法角度学习总结,所以在软件设计章节总结过去被省略、或经常被讨论但是彼此都很难说服对方的内容进行总结。还是延续本文银行案例,因为无银行经验,可能设计细节会有出入,但是不妨碍我们通过分享和讨论逐渐理解模糊部分。
我会挑以下部分进行自己知识结构的梳理,分别是状态机、充血还是贫血、一定要聚合根吗。
状态机
通过下图读懂状态机图示表达,需要特别记住的知识点:从A状态转化成B状态的箭头直线,描述了某个「事件」发生时,恰好某个「条件」成立,通过具体「动作」,实现状态A到状态B的转化。
比如准备「睡觉」这个事件发生,此时「灯亮」这个条件成立,通过「语音关灯」指令完成关灯,将「灯亮」状态变成了「灯灭」。
同时状态机中还有一个知识点需要记住:如果多种事件导致同一个状态发生,可以直接在目标状态中描述,比如下图中的灯灭有3种转化,分别是睡觉[灯亮]/按下按钮、睡觉[灯亮]/语音关灯、限电[灯亮]/拉下总闸。
有了对状态机的理解,下面开始了解为什么状态机很重要。
领域模型中的领域对象有生命周期,意味着生命周期变化过程中会因为不同事件发生,带来领域对象不同状态变化。如果状态演变流程复杂且多状态,会大大增加系统复杂度及状态准确性,如果缺乏状态机的有效设计,会增加代码处理的复杂度,进而带来Bug及用户不满意。
解决的方法就是基于状态机描述清楚领域对象状态流转的业务知识,然后用代码实现。还是以银行为例,比如领域对象「资金」,资金有交易状态,包括资金冻结和资金正常状态。可以设计如下的状态机。
上图状态机把因为各种事件导致的状态变化集中在了资金冻结和资金正常2个状态中,当然也可以把各种事件都画出来体现图复杂性,但是我觉得必要性不高,会增加理解成本。
同时资金交易状态,我只定义了“资金冻结和资金正常”,实际银行的资金状态可能不是这2个状态,但是没关系,我们要达成的目标:通过状态机清晰定义哪些事件导致状态变化,把这些事件用代码实现它,就能规避很多问题。
另外,状态流转背后是复杂业务知识流程,当多个领域对象都存在状态变化,我们要避免在一张状态机图中体现出所有的状态流转,那样起不到简化业务知识的效果,反而增加理解的难度。
比如账户有很多状态,比如开户中,冻结中,风控异常,正常等。如果把账户状态和资金状态放在一张状态机会大大增加阅读难度和理解成本。如果再加一个领域对象的状态在一张状态机中,复杂度会再次增加。
充血还是贫血
贫血还是充血一直被争论,其中2个争论较多:
(1)业务逻辑放在实体对象,还是Servie中?
(2)实体对象是否需要依赖Repository?不同的人实践结果不一样,谁也无法说服谁,这也是为什么将“充血还是贫血”作为一个小节。
争论1:业务逻辑放在实体对象,还是Servcie中?
贫血模型:一个领域对象的业务逻辑实现由一个服务类和实体对象完成,实体对象包含领域对象中的属性和数据,服务类中包含领域对象的所有方法,负责具体的领域逻辑处理,及持久层的调用。也可以简单理解为领域的行为都由服务类对外暴露,如下图:
充血模型:领域对象的业务逻辑实现同样由服务类和实体对象完成,但是实体对象不仅包含领域对象中的属性和数据,还包括具体的方法,服务类仅仅面向外部暴露接口,具体的逻辑处理和持久层处理由实体对象完成,如下图:
通过上面2张图,看起来充血模型要优于贫血模型,因为所有逻辑都封装在领域对象。但究竟贫血模型好,还是充血模型好?争论不休。但可以确定的是,一个团队或一个系统来说应该统一模型,不能一个系统中既出现贫血模型,又出现充血模型。
当然,如果只是理论讨论,不涉及代码实现,大部分人可能会赞同使用充血模型,核心原因是充血模型还原了领域模型的原貌(从模型到代码一致性),包括所有的组合,聚合对象的封装等,比如资金领域模型,包括活期资金、定期资金、资金明细等。这些关系在领域模型中体现的是业务的复杂度,但如果把复杂度平移到代码中,可能会增加代码的复杂度,这也是为什么,很多代码反而是贫血模型,而不是充血模型的关键所在。
当然,现实中更多采用贫血模型,还有另外一个原因:充血模型需要更强的领域设计能力和对象抽象能力。而贫血模型把逻辑放在Service中,面向过程编程,或面向步骤编程,可能更符合我们编写代码的思路,从第一步...第N步。
如果要问自己的选择?基于自己的编程经历,会选择贫血模型。主要有以下几个原因:
- 无论是实体对象,还是服务类,本质上都是对外暴露行为,上层调用者不关心实体对象或服务类的内部实现,而且从命名上可以方便看出领域的含义,比如FundService和FundEntity。
- 服务类灵活性高,如果一个领域对象的逻辑非常复杂,把所有领域逻辑封装在一个实体对象里,估计实体对象会爆炸,既包含数据,也包含行为,反而给上层使用带来负担。但是服务类可以通过职责拆分,比如拆分成资金取款服务类、资金转账操作类等,这些服务类,Repository和实体对象共同组成了领域模型的实现。
- 经典三层分层(Controller、Service、DAO)将领域模型进行了切割,每一层职责清晰,也容易理解。同时因为实体对象反应的是业务领域的关系,这个关系并未因为分为三层被破坏。
争论2:实体对象是否要依赖Repository?
关于数据的持久化存储放在哪里?也是贫血模型和充血模型的争论点,如果是业务逻辑为什么要关心数据的持久化还是非持久化呢?如果内存足够大,是否可以直接在内存中运行呢?通过极端条件讨论理论指导的正确性,有时候也是一种方法。所以贫血模型是把数据存储从领域模型中分离出来,变成基础设施层是一种指导思想。
领域模型的存在不应该依赖任何技术框架,是纯粹对业务的逻辑抽象。但是如果使用充血模型,充血模型会依赖Respository,那么充血模型的Respository操作必须只和本领域相关,如果跨多个领域模型的操作,就破坏了领域。
面向聚合根统一操作
聚合的一个主要特征是具有不变规则。而维护不变规则的前提是要做好对聚合的封装,否则,外部的对象就可能无意间破坏聚合内部的规则。
意味着,服务类的行为入参都是聚合根,比如,以人为例,人是四肢和器官的聚合根,如果四肢或器官受伤了,肯定需要把人送到医院才能做治疗,不能把四肢砍下来,或者把器官割下来送到医院治疗,治疗之后再运回来安装上去,即便四肢和器官治疗好了,但是人估计早就没命了。同样现金账户也是同样的道理,在操作定期还是活期,都需要通过现金账户这个聚合根。站在技术实现的角度,通过聚合根还能够保证事务的完整性。
聚合根的逻辑非常符合我们的思维模式,相信在理论层面的讨论没人会拒绝“面向聚合根统一操作”的指导方式,但为什么现实情况又有这么多的争议,而且实际代码的实现往往是破坏这个指导方式。
我认为本质原因有如下几点:
- 基于聚合根操作给代码实现带来了复杂性,每次操作聚合对象都需要通过聚合根来实现。
- DDD思想出现在20年前,当时的软件系统很多是桌面端或者单机,像互联网这种分布式系统还未流行,通过聚合根实现事务比较简单。但是分布式系统下的分布式事务下,事务的保障并非聚合根的思想就可以解决。
- 现实中对聚合对象的操作虽然没有基于聚合根,但并未破坏“聚合根思想”,这点很多时候并未被提出来讨论过。比如,日常操作聚合对象的时候,聚合对象上都有聚合根的ID,同时如果涉及聚合对象变更之后,聚合根也要变化(反之),在Service中会按照面向过程编程,把聚合根也做变化。
所以,我的观点是“面向聚合根统一操作”的指导思想,是告诉我们一个聚合内部的逻辑要保持一致性(就像事务一致性一样),每次变更聚合根(比如删除),对应的聚合对象也要变化。而不是每次都要new聚合根和new聚合对象,这样确实给代码编写带来了很多成本和复杂度,可以通过聚合根ID来操作对应的聚合根和聚合对象。
应用分层
领域模型是解决业务复杂度的问题,代码是领域模型的具体实现。可以直接从领域模型平移到代码(比如DDD中倡导的聚合根统一操作),也可以采用分层架构思想。但是,代码的实现是否要完全从领域模型平移过来,我自己是打个问号的?
一方面很少见到直接从领域模型平移过来的实现,另外一方面,自己的编程习惯或CR看到的编程习惯,大部分还是面向过程,面向步骤的编程。所以以下给出的应用分层只是参考建议,各自可以参考,也可以调整。但是有些原则需要得到遵守:
- 依赖倒置:有2个含义,分别是(1)高阶模块不能依赖低级模块,比如基础设施层不能依赖应用层;(2)依赖抽象,不依赖实现,层与层之间的依赖通过抽象依赖。
- 服务粒度:也有叫界限上下文,不管何种叫法,服务的拆分粒度只要拿出来讨论,肯定会有不同的观点。我的观点,物理粒度无需过多讨论,需要的时候自然而然会拆分,比如并发上来了会拆分多个服务,从单体应用多分布式应用。所以我们更侧重在逻辑粒度上,逻辑粒度可以按照聚合角度拆分,未来需要做物理拆分,可以将单体应用中的聚合拆分即可,而且因为低耦合快速实现物理拆分。
1.横向每一层
Adapter层:适配层,个人不太喜欢该名字,也许用网关更合适,负责接受来自不同设备的请求和响应,包括请求和响应之间的安全校验、登录验证等。Adapter可以向下依赖应用层,依赖的方式遵循依赖倒置原则,通过抽象Client二方包(接口定义和DTO定义),实现依赖。
应用层:向上暴露领域对象的行为(叫方法也行),向下执行各领域服务的编排、调用和结果封装。应用层不应该包含领域的业务逻辑,是很薄的一层。同时应用层不能向上依赖Adapter(通过Client接口抽象暴露领域的行为),可以向下依赖依赖领域层和基础设施层,但是层与层之间的依赖遵循依赖倒置原则。
领域层:领域的业务逻辑在领域层实现,包括领域服务、实体、值对象等,甚至还包括DO,这里需要说明下我自己的观点,DAO和DO等不属于基础设施的实现,只是我们在使用基础设施时做的一个封装,应该放在领域层。同时,领域层还会涉及究竟是用充血模型和贫血模型,个人的经验更倾向于贫血模型,具体的原因参考《5.2 充血还是贫血》。领域层是业务逻辑的内聚,势必会涉及到DB存储,缓存提速,文件处理等,对这些基础设施的依赖遵循依赖倒置原则。
基础设施层:主要是各类基础设施,包括DB、Cache、File等,这些基础设施一般都会提供各自的Client包,领域层基于Client可以完成对基础设施的调用。
2.纵向每一列
每一列的划分基于聚合维度,比如还是以银行为例,资金是一个聚合维度、贷款是一个聚合维护、账户也是一个聚合维度,各自应该有独立的Adapter层、应用层、领域层、基础设施层。但是物理结构上有可能是独立,也可能是在一个应用中。
总结
软件方法是一套思想或方法论,从业务理解到软件设计一体化的思想和方法,在思想和方法论被不断运用和实践之后,相对应的各种能力会逐步达到熟练状态(比如业务用例,业务时序,UML使用,领域建模等),自然而然能够提升工作交付的效率和质量,职业发展和软件质量会因此受益于软件方法背后的抽象能力、架构能力、及高效高质量的交付能力得到认可和发展。
每位技术人员应该优先侧重思想背后的能力提升,也许1年,也许3年。如果像本文一样,边学习边总结边输出,撑死1年,快的话半年基本可以熟练掌握软件方法的思想,未来再具体实践完善不足和理解不到位的地方。
参考资料:
潘加宇:《软件方法-上》
钟敬:《手把手教你落地DDD》
作者 | 聪安
点击立即免费试用云产品 开启云上实践之旅!
本文为阿里云原创内容,未经允许不得转载。