3.3.1.3 用例集
用例集是一组相关的成功和失败的场景集合,用来描述参与者如何使用系统来实现目标。单独一个用例,应该要能完整地表达一次或一类业务行为。
比如 [拥有88vip的消费者][在购物车同时勾选两个同店铺活动商品时],[点击查看明细按钮],[经过同店活动限购1次的优惠计算规则],[得到只享受一个优惠的价格]。
其范式为“参与者(可以是系统或者用户)+前置条件(在什么场景下)+如何使用系统+经过什么规则+产生什么结果”。优秀的产品的prd文档,应该要包含无限接近于标准的格式化用例描述集合,它是原始产品方案通过多轮沟通对焦,得出来的标准化语言。
这里的用例集和我们常说的tc(测试用例)集是有差别的;用例相比tc会更强调过程规则,强调对功能的描述;tc则更关注的是输入和输出,强调覆盖场景,强调对业务功能、业务模型、技术模型的功能性/非功能性验证。
- 3.3.2 架构描述
大多数同学都不喜欢写技术方案、架构文档,因为它会占用写代码的时间,而且由于返工和迭代的必然性,架构文档相比代码往往显得过时。但是优秀的架构描述是有用的资产,在多人协作的场景下,在系统需要被多方了解评估或开发测试时,能有效地促进沟通和协作,将设计决策和思想有效地传递给每一个人,提高软件的开发测试质量。口口相传的信息传递效率永远都是最低的。
无论是3.2中讲到的业务模型,还是软件系统模型,对其架构的描述,都不可避免地涉及到通过画图来描述全貌。复杂的系统架构,想用单一的模型图来描述,一般只能粗具梗概;所以 ISO/IERC/IEEE 42010:2011 中提出了“视点”的概念,并做了定义:
视点:从不同的视角或者专业领域来看待系统的方法。
常见的视点方法论有以下这些。
- C4模型
Simon Brown
- 上下文层次(Context):描述了系统与外部实体(如用户、其他系统、硬件设备等)之间的关系,显示系统如何与其周围的环境交互以及其外部依赖关系。
- 容器层次(Container):系统内的软件被分解成多个容器,如应用程序、数据库、文件系统等,容器图描述了这些容器之间的关系及它们如何共同工作以实现系统的功能。
- 组件层次(Component):在容器的内部,每个容器被进一步拆分为组件,如类、模块、服务等,组件图描述了组件之间的关系和依赖关系,以及它们如何协同工作。
- 代码层次(Code):这是最低层次,描述了每个组件的内部实现细节,通常可以是类图、包图等,用于展示组件内的代码结构。
- 4+1视图模型
场景视图:从外部视角,描述系统的参与者(用户)与系统功能用例的关系。反映的是系统的最终用户需求和交互设计。
逻辑视图:从结构化视角,描述该系统对用户提供的所需功能服务所具备的组件结构和数据结构,以及一些边界约束条件,清晰的描述给用户提供的功能需求服务是如何构建的。描述该系统内部所具备了那些组织结构,以达到实现对外功能。
开发视图:从结构化视角和行为视角,去描述实现系统功能的各个组件和模块是如何实现的。
处理视图:从行为视角,描述系统各个组件和模块是如何进行通信的。
物理视图:从交互视角,描述系统可以部署到哪些物理环境(如服务器、PC端、移动端等)上和软件环境(如虚拟机、容器、进程等)上。
Phillipe Krutchen
本文从下面两小节的问题空间和解决方案空间的模型来做架构描述:
业务领域模型图:描述需求的分析结果,突出业务领域概念和业务模型关系,统一产品需求方、领域专家、开发人员的概念语言;
系统模块图:描述系统内的分层模式和模块/领域依赖关系,描述系统间的依赖关系和数据交互方式;
对象模型图:描述核心类职责,类与类继承关系,类与类的依赖关系;
数据存储架构图:描述库表结构,分库分表策略,存储选型,以及不同数据表之间的一对多/一对一等依赖关系。
这些讲的都是一个视点想要表达的内容,而不是只用有一张图来表达的内容;为了让系统的细节更丰富地呈现,我们可以按需绘制精细视图,来描述某个局部的系统细节;为了表达设计原则,我们可以增加质量属性视图,描述对于某个质量属性的设计,如可用性、数据准确性、一致性等,突出质量属性是这个视图可以表现的故事,因为质量属性通常不那么清晰可见,容易被忽略,我们可以在这里将它化虚为实。
对于单个视图的绘制,UML是一个比较完善的标准,它可以很好地表现设计构思,但是并不是所有人都对UML了如指掌,我很赞同Simon Brown的观点,“虽然UML很有用,但我更喜欢用简单的方框和线条表现架构。为避免混淆,我建议尽可能使用简单的、不言自明的符号,并添加必要的图例”,这里表现的就是尊重架构受众的思想,因为设计的本质是社交,我们要让我们的设计以更高的效率传递到其他人那里,包括技术人员和非技术人员。
- 3.3.3 业务模型
业务模型是问题空间的领域模型,描述业务和产品,与软件系统无关,具体描述的是客观物理世界的概念、规则、关系。
要对业务领域建模,首先需要对3.3.1中的用例进行分析,引用前辈的做法:
a、从准确的用例中剥离出名词;
b、根据名词梳理领域模型和其属性;
c、根据名词的修饰梳理出属性值;
d、根据名词的定义完善属性值;
e、从用例集合中剥离出动词&形容词;
f、根据动词&形容词梳理出领域模型之间的关系;
但仅仅是对用例集进行提取,会遗漏一些隐藏概念,比如关于“一个店铺拥有多个子账号,每个子账号对店铺的可操作权限不同”的描述,我们很容易将操作权限作为子账号的一个属性,子账号则挂靠在店铺实体上
相信做软件开发的大家对这个例子应该很亲切,因为我们一般都会将权限拎出来,作为单独的实体概念,再将权限关联在帐号上,也方便权限的维护的后续的继承:
这里就涉及到架构元模型(元模型定义了模型中使用的概念和使用规则)中隐藏概念的建立,好奇心循环是我们建立元模型的手段, 从提问开始,建立模型、检验模型、分离概念。
关于前面讲的提取的名词(概念和属性),还有很重要的需要注意的点是,这些词语需要与各个相关方统一语言;Michael Keeling和Eric Evans在他们的书中,都不约而同地强调了业务与技术之间统一通用语言的重要性;如果产品需求方、领域专家、开发人员对业务模型的描述语言都局限在自己的圈子内,那么沟通将引入巨大的翻译开销。有过跨团队协作的同学应该能够深刻理解这种翻译带来的误解的风险和成本有多高,并且这种翻译将使得沟通不畅,各方在做领域知识消化的时候,都需要耗费额外的精力。
所以在业务模型的设计时,开发人员与业务相关方之间要建立通用语,所有利益相关方都有权理解架构,有权知道系统中的各个模块是如何协同运作。
- 3.3.4 软件系统模型
3.3.4.1 系统模块架构
系统模块架构核心描述系统内的分层模式和模块/领域依赖关系,描述系统间的依赖关系和数据交互方式;
关于系统分层模式和模块/领域的依赖关系方法论,《DDD中常提到的应用架构总结(六边形、洋葱、整洁、清晰)》中有一张图画的非常好,一图即可概览主流架构分层思路在ddd方向上的演化,这里引用一番:
最开始的MVC架构,对系统做简单分层,描述了从数据层到业务逻辑层到数据出口层的关系;由于mvc中,业务逻辑对数据出入口的依赖是确定的,所以这个依赖变化时,改动成本高,无法突出领域模型的独立性;
于是六边形架构横空出世,它将领域层独立出来,不依赖任何确定的外部服务,以端口/适配器的方式定义外部服务交互协议,只要能实现这套交互协议,数据出入口的依赖变化,对领域层是没有侵蚀的;这里体现的是领域驱动设计的思想,将领域知识立在最重要的地位,不为任何模块影响。
最后是清晰架构,它的最内层是领域层,包含领域模型和领域服务,实现相关的领域知识和概念模型;向外是应用服务,可以依赖领域层,做业务用例的编排实现,比如操作某个领域服务后,再操作某个实体进行某项行为,最后发送某个领域事件等;和应用服务层同级的,还有CQRS和事件/消息处理器,接受不同类型的命令执行类似应用服务的事情;以上这些即应用核心,应用核心与外界的交互分为两类:图中的左半边为主动适配器,做类似于Controller或是HSF服务的系统最外层请求实现;图中的右半边为被动适配器,定义消息出口、数据持久化接口、搜索引擎接口等,由外部具体的基础设施实现。可见,清晰架构属于集前人所长,提供的一份以领域知识为核心的分层架构指南。
除了系统内部的分层结构,我们还需要描述系统间的关系,应用间的关系,以及数据流转的方式,如下图:
3.3.4.2 对象架构
按照3.3.3中抽象出来的业务领域模型来编写代码,能够使代码更好地表达设计含义,并且使模型与实际的系统相契合;面向对象编程之所以强大,就是因为它为架构概念提供了实现方式,能描述现实物理世界中的关系(操作、继承、组合)和模型(定义、属性、职责)。
《领域驱动设计,软件核心复杂性应对之道》中有大量的篇幅讲解用面向对象的思路对类的类型进行划分,并将业务模型映射到对象模型中的模式:
- 实体:由标识定义,而不依赖它的所有属性和职责,并在整个生命周期中有连续性。这句话在初看的时候非常晦涩,简单来说,就是一个标识没变的对象,在其他自身属性发生变化后,它依然是它,那么它就是实体;举个例子,一个订单的收货地址,收货人电话,都发生了变化,但是这个订单id没有变,那么这个订单依然是变化前的那个订单,只是它的一些属性发生了变化;通过这种方式来识别实体的目的,是因为领域中的关键对象,通常并不由它们的属性定义,而是由可见的/不可见的标识来定义,且有完整的生命周期,在这个周期内它如何变化,它都依然是它;通过这种方式识别出实体这种领域关键对象,也是领域驱动设计和数据驱动设计最大的差别,数据驱动设计是先识别出我们需要哪些数据表,然后将这些数据表映射为对象模型;而领域驱动设计是先通过业务模型识别出实体,再将实体映射为所需要的数据表。不过前面也提到,实体的标识可以是可见的,也可以是不可见的,因为有很多域内无持久化的系统,在它们的对象模型中,并不存在可见的唯一标识id,所以在我之前的另一篇文章,也提供过不一样的描述实体的思路:
对于更加关注"行为"而非"唯一性"的纯计算型应用,给出划分实体与值对象的另一种思路:
1、实体是会对自身属性做出强解释行为的类型。
2、值对象是轻属性解释,重属性设计的类型。
理由是,纯计算型应用,业务关注重点是行为,当一个类需要承载复杂的计算逻辑,即对自身属性需要进行强解释行为时,它往往就承载了系统中更重要的职责,能更加凸显领域业务概念。
- 值对象:用于描述领域的某个方面而本身没有唯一标识的对象。被实例化后用来表示一些设计元素,对于这些设计元素,我们只关心它们是什么,而不关心它们是谁。举个例子,一个订单的收货地址Address对象有省份、城市、街道、门牌号这几个属性,其中的门牌号从111修改成222后,它就已经不再是修改前的那个它了,因为门牌号222并不等于门牌号111的地址。即它是没有生命周期的,它的equals方法由它的属性值决定(实体的equals方法由唯一标识决定);
- 聚合:聚合是一组实体和值对象的组合;内部包含一个聚合根,和由聚合根关联起来的实体和值对象;比如说有商品、sku、库存三个实体,那么在商品模型中,商品就是聚合根,其内部通过sku id关联它的sku,通过库存id关联商品/sku的库存;聚合将这组关联关系建立,对外提供统一的操作,比如需要删除某个商品,那么这个聚合的内部可以在一个事务(或分布式事务)中,对库存进行清空,对sku进行删除,最终对商品进行删除。
- 服务:有一些对实体/聚合/值对象进行编排操作的概念并不适合被建模为对象,那么它应该被抽象为服务,化作一只上帝之手,做领域对象间流程操作的编排。服务很重要的特征,它的操作应该是无状态的。
- 工厂:当创建一个实体对象或聚合的操作很复杂,甚至有很多领域内部的结构需要暴露的时候,就可以用工厂进行封装。一种相对简单粗暴的判断方法是看这个类的构造方法实现是否复杂,并且看着这些逻辑不应该由这个类实现,那么不妨用工厂来构造这个对象吧!
- 仓库:仓库是可持久化的领域对象和真实物理存储操作之间的媒介,随意的数据库查询会破坏领域对象的封装,所以需要抽象出仓库这种类型,它定义领域对象的获取和持久化方法,具体实现不由领域层感知;至于具体用了什么存储,如何写入和查询,是否使用缓存,这些逻辑统一封装在仓库的实现层,对于后续迁移存储、增删缓存,都可以做到不侵蚀业务领域。
- 防腐层:防腐层并不是一个特定的对象类型,而是一种领域模型保护的思路;对于领域外界的变化,我们需要持悲观的态度,因为领域外部的模型不受我们控制,它们的变化轨迹难以捉摸,所以在系统与系统直接,上下文与上下文之间,要有一层放腐层进行领域内外的模型转换。
有了对象类型的划分,对象职责如何确定呢?GRASP给出了很好的判断标准:
创建者
问题:谁负责产生类的实例
解决方案:如果符合下面的一个或者多个条件,则可以将创建类A实例的职责分配给类B
B包含A
B聚合A
B拥有初始化A的数据并在创建类A的实例时将数据传递给类A
B记录A的实例
B频繁使用A
信息专家
定义:如果某个类拥有完成某个职责所需要的所有信息,那么这个职责就应该分配给这个类来实现。这时,这个类就是相对于这个职责的信息专家。
解决方案:将职责分配给拥有履行一个职责所必须信息的类(域)。
低耦合
问题:怎么样支持低的依赖,减少变更带来的影响,提高重用性?
解决方案:
在类的划分上,尽量创建松耦合的类,修改一个类不会影响其他类。
在类的设计上,尽量降低类中成员和方法的访问权限,尽量将类设计为不变类。
在类的引用上,将一个对象对另一个对象的引用降低到最小。
高内聚
问题:如何使得复杂性可控?
解决方案:功能性紧密的相关职责应该放在同一个类中,并共同完成有限的功能。这样做更加有利于对类的理解和重用,也可以降低类的维护成本。
纯虚构
问题:当不想破坏高内聚和低耦合的设计原则时,但是有些职责又没地方放,如何处理
解决方案:将一组高内聚的职责分配给一个虚构的或者处理方便的类,它并不是问题域的概念,而是虚构的概念,以达到支持高内聚低耦合和重用的目的。
间接
问题:如何分配职责,以避免两个事物之间的直接耦合?
解决方案:当我们不知道将职责分配给何种模型的时候,可以看看是否可以将职责分配给中介模型。
3.3.4.3 存储架构
存储架构描述库表结构,分库分表策略,存储选型,以及不同数据表之间的一对多/一对一等依赖关系;常见的手段有E-R图描述表模型关系,用图例来描述分库分表策略以及存储选型策略。
总结
这篇文章从动笔到写完,断断续续持续了一个多月。对自己也是一次总结和思考的过程。
文章中可以看到,创造/成长类的方法论,很多环节的都离不开【循环】。像学习->实践->反思的循环,像架构设计中 分析->设计->返工->反思的循环,像好奇心循环中的 提问->建模->验证->分离概念的循环,以及文中没有提及的也很有名的TDC循环;可见恩格斯所说的螺旋形式上升之精辟,任何事物都是从肯定到否定再到否定之否定中发展。
对架构师的思考和实践也一样,本文是站在当前的视角和经验进行的总结,大家可以指出其中不妥的部分,让螺旋形式的发展可以一直循环下去。
回到文章的开头,有提到横向架构师和纵向架构师的概念,但本文通篇在讲纵向架构师的故事;因为,横向架构师本质也是站在多业务域视角上的纵向架构师,单业务领域对它来说变成了子域罢了,方法论是相通的;另外横向架构师一个很重要的职责就是本文的标题,让人人都成为架构师。
参考资料
- 《DDD中常提到的应用架构总结(六边形、洋葱、整洁、清晰)》:
https://code84.com/730128.html - List of Quality Attributes for Grid Monitoring Tools:
https://www.researchgate.net/publication/251818235_List_of_Quality_Attributes_for_Grid_Monitoring_Tools - The C4 model for visualising software architecture:
https://c4model.com/ - 《架构师修炼之道》,作者:Michael Keeling,译者: 马永辉 / 顾昕,出版社:华中科技大学出版社
- 《领域驱动设计,软件核心复杂性应对之道》,作者:Eric Evans,译者:赵俐 / 盛海艳 / 刘霞,出版社:人民邮电出版社