领域建模的体系化思维与6种方法论 - 阿里技术
本文希望能够通过总结过去自己对领域建模的一点粗浅经验给需要的同学能有些许启发,少走弯路。
背景
软件工程师做的核心事情就是对现实世界的问题进行抽象然后用计算机的语言对其进行重新刻画,在通过信息化来提高生产力。而这其中一个关键环节就是如何对问题域进行建模,在过去的工作中经常遇到一个问题是前期因为业务比较简单所以设计的模型在支撑时没有发现什么问题而随着业务复杂度的增加就会发现需要对模型进行升级,如果是对模型关系维度的新增那还好,而如果是对原有模型关系的重构,那将会变的非常困难。本文希望能够通过总结过去自己对领域建模的一点粗浅经验给需要的同学能有些许启发,少走弯路。
总体思想
关于如何建模并不是一个单一维度的问题而是一个体系化的工程,我们需要对其进行拆解然后逐个攻破,如何建好模并能顺利落地可以拆分为四个子问题,1)对需求进行功能建模 2)对业务进行领域建模 3)将领域模型映射到代码模型 4)根据代码模型落地数据模型。
- 需求模型:通过和产品及业务同学的沟通,结合行业经验和知识,明确用户的真实需求;
- 领域模型:基于需求模型,提炼出领域相关的概念,为后面的面向对象设计打下基础;
- 代码模型:以领域模型为基础,综合面向对象的各种设计技巧,完成类的设计;
- 数据模型:以代码模型类及类之间的关系,用ER图刻画数据的底层存储关系;
还有一个重要的认知,领域建模并不是万能的,比如你做的系统很简单,使用数据库的CRUD可能一个更好的方式(如上图中的虚线箭头),如果做的是基础架构,比如开发一个RPC框架,也不需要用领域驱动。运用领域驱动的前提一定是业务足够复杂且多变,需要系统灵活支持。下面这个图供参考:
需求建模
万事开头难,需求是项目最开始的输入,肯定是非常重要的,但现实情况往往是当我们接到需求后第一个想到的问题确是我该如何实现,而没有在业务思考这一块过多的停留。如果最开始理解的输入是错误的,后面的过程再优秀可想而知也只能是垃圾。为了解决这一问题,很多优秀的大佬发明了很多对需求进行功能建模的工具,主要目的就是让我们更好的理解需求并认识其背后的本质。如常用的工具有5w2h、业务活动、用户旅程、商业画布等。这些在网上都有很多的资料,只举几个例子,不做重点介绍。
- 业务活动
- 用户旅程
- 商业画布
领域建模
下面的每一种方法都是从某一个角度帮助我们更好理解客观事物,所以没有哪一种方法是万能的,在实际项目中常常要多种方法共用,才可能认清事情的全貌。
四步建模
在我们日常工作的项目中,大多数场景都可以通过在需求用例中通过找名称的方式来最终刻画出领域模型,当然找到的名词后并不是所有的都符合要求,这时可以通过一条原则"一个名词或实体必有其属性和行为,一属性或行为也必归属于一个实体"来进行提炼,不符合这条原则的名词就是需求剔除的。总体来说建模一共分为四步
- 选名词:从用例中选出所有名词,在去伪存真选出符合要求的;
- 找动词:找出所有动词,在判断这些动词属不属于上一步选出的名词所具有的行为;
- 加属性:找出所有属性,在判断这些属性属不属于上一步选出的名词所具有的特征;
- 连关系:确定实体和实体之间的协作关系;
案例:用户购买商品
需求用例
用例名称 | 用户购买商品 |
---|---|
用例描述 | 1、用户在购物app上选取多款商品进行下单 2、通过用户档案获取用户名称、地址等信息 3、从不同商家购买的商品生成不同的子订单;记录商品及其数量,并汇总付款金额 4、保存订单 5、选择一种支付方式(包括银行卡、支付宝、微信)并进行支付;超过30分钟未支付自动取消订单 |
约束和限制 | 1、一个商品单次购买不能超过30单 2、单次支付金额最大不能超过99999RMB |
建模步骤
第一步:选名词。从用例上选的名词如下:用户、购物app、商品、用户档案、用户名称、地址、商家、订单、子订单、支付方式、银行卡、支付宝、微信。通过这种方式可以很轻松的识别领域中的相关概念,但选取的名词并不一定都是领域相关的,所以接下来还需要进一步的提炼。提炼过程
- 删除"购物app":购物app只是一个功能的载体,并不属于购买商品流量里的一个领域概念,所以删掉
- 删除"用户名称" :用户名称只是用户的一个属性,并不是领域概念
- 删除"地址" :地址只是用户档案的一个属性,并不是领域概念
- 删除"银行卡、支付宝、微信、支付方式":银行卡、支付宝、微信属于支付方式的一种具体形式,而支付方式可以归属为订单的一个属性,并不是独立的领域概念
所以最终提取的领域实体是:用户、商品、用户档案、子订单、订单、商家
第二步:找动词。从用例上选的动词如下:选取、汇总、下单、保存、支付、取消
找动词的目的是反向检查是否有遗漏的实体没有提炼出来,因为有些隐含的概念并不一定能在用例里找到,且一个动作必归属于一个实体。如果有发现动作没有归属实体只有2种情况,一是这个动作不属于这个领域,二是有遗漏的实体没有提取出来。经过分析 "选取" 是用户主观的一种行为,并不属于这个领域所以删掉,"下单、汇总、保存、支付、取消" 都属于订单的动作。
第三步:加属性。理论上产品同学要在用例上把模型的所有属性全部列出来,但现实情况不一定能做到,这时除了用例还需要当面和产品对焦清楚各个模型的属性。
名词 | 属性 |
---|---|
用户 | 用户名称、性别、证据类型、手机号 |
商品 | 商品名称、单价、商家、商品类别、商品编码 |
用户档案 | 国家、省份、地市、区县、地址详情、联系电话 |
子订单 | 商品、数量、单价、金额 |
订单 | 用户、地址、金额、下单时间 |
商家 | 商家名称、经营地址、经营范围、联系人、联系电话 |
第四步:连关系。关系主要表达模型和模型之间怎样协作
补充场景:比如 "一个客户想入驻平台成为一个服务商",按照上述方法提炼出动词"入驻",但会发现其并不适合成为"客户"、"平台"、"服务商"实体的行为,因此可以反向推出还应存在另外一个实体"入驻申请单"的概念,"入驻" 这个动词更准确的叫法是"申请入驻"。
归类分组
归类分组,就是把具有相似性或相关联的信息,按照一定的标准进行分类,归为同一个逻辑范畴。“类”是一个极其重要的概念,可以看作本质相同或相似的事物的集合。分类就是按照一定的标准,根据对象属性、特征的共同点和不同点,将对象划分为不同的种类。分类时,需要对这些类别进行鉴定、描述和命名。
分类在生活中无处不在。通过分类,一方面可以把杂乱无序的事物区分开来;另一方面还要赋予不同类别以稳定的、概念化的名称。一个好的命名,能够简洁有力地表述事物的本质。分类的过程,就是探寻事物和问题本质的过程。
如何用归类分组进行领域建模可以分为3个步骤
第一步:定义要建模的领域问题:也就是要清楚我们要解决的问题是什么?
第二步:对领域问题进行拆解:对问题进行分析拆解,形成平铺的多个子问题,此步骤可以尽量发散
第三步:归类分组:对子问题进行归类,剪枝,将趋同的子问题,合并成一类(可以理解问产出实体)
案例:生活服务类商品
生活服务类商品场景
-
预定类商品(KTV预定、休娱预定、丽人预定等):
-
可以按照周期设置每周的价格;支持设定节假日的价格;
-
业务特点:商品价格同样呈周期性变化,有特殊日期以及不可用日期;计价方式包括按人、按间
- 场馆预定类
归类分组建模过程:
- 第一步:定义要建模的领域问题:从上面的售卖商品场景可以看出一个商品有多个价格;而决定这个价格的是由一系列的因子决定的,有可能是0个也可能是多个;如何用一套完备的价格模型来支持这些场景是需要解决的问题;
- 第二步:对领域问题进行拆解:罗列出所有商品价格的场景;
-
第三步:归类分组:
-
按人群、日期、阶梯、座位等类型,可以统一抽象为价格的不同"维度"
-
原价、起拍价、保证金、评估价等可以看做室价格的不同类型
所以一个价格由什么决定:sku的价格 = 价格类型 + 价格维度 + 具体价格,具体抽象过程如下:
- 第四步;产出领域模型
案例:本地通商家成长
商家成长:为不同层级的商家门店下发不同类型的任务,完成对应的任务,商家会获取到对应的权益,进而帮助商家在平台的不同阶段能够更好的成长。
-
第一步:定义要建模的领域问题,针对门店维度为商家建立一套任务体系,不同的门店可以下发不同的系列任务
-
第二步:略
-
第三步:
-
基础装修、商品优化、服务评价等属于一个维度,可以抽象为"任务流"
-
门店图、手艺人、是否有品牌故事等属于一个维度,可以抽象为"任务"
-
而由"门店图、手艺人"等任务组成的基础装修,可以抽象为"任务组"
-
门店图片里又包含指标和完成的跳转动作,可以分别抽象为"任务指标","任务动作"
-
第四步;产出领域模型
事件风暴
为什么引入事件风暴。
事件风暴之一的作用就是拉通业务方、产品、研发、测试对业务知识的统一理解,避免各方理解误差。但在实际操作中受限于各方时间协调的难度及领域专家的角色的缺失,事件风暴往往作为理解业务,领域建模及领域划分的利器去使用。
什么是事件风暴。
事件风暴是一种以工作坊的方式对复杂业务领域进行探索的高效协作方法,事件风暴强调以事件驱动团队探索分析业务领域。
-
一种捕获行为需求的方法
-
强调以先发散后收敛的方式开展
-
相关干系人协作的方式进行
-
注重对领域事件的识别
事件风暴相关元素
-
事件:重要且已发生的事情。命名方式:完成时+被动语态=宾语+动词过去式
-
命令:产生事件触发的动作
-
角色/执行者:触发命令的主体,包括:人员、系统、定时任务
-
读模型:执行者决策执行命令时参考的视图元素
事件风暴操作流程
-
识别重要事件
-
识别命令
-
识别执行者和读模型
-
识别领域对象
-
产出领域模型
案例:销售基础
产品截图
- 识别重要事件
- 识别命令
- 识别执行者和读模型
- 识别领域对象
这里说的领域对象,是从命令、领域事件、执行者、查询数据里找到的名词性概念。例如,对于签订合同这个命令而言,受到影响的名词性概念是“合同”;类似地,对于合同已签订这个领域事件,是由于“合同”这个名词性概念的状态变化所导致的。
- 产出领域模型
四色建模
关于四色建模的概念我们可与追溯到90年代,是被广泛使用的一种系统分析方法。四色建模的目的是要对目标业务系统进行分析并通过不同的颜色标示出人,事,物,角色,通过四色建模得到四色原型图,每个原型图有属性和连接(关联与依赖等关系)两个部分组成。
那四色原型具体是哪四色呢?一起来看看
-
时标原型(Moment-Interval Archetype,简称MI)
-
表示事物在某个时刻或某一段时间内发生的,如销售订单、收款记录等,使用浅红色表示。
-
PPT原型(Part-Place-Thing Archetype,人/事/物原型,简称PPT)
-
表示参与扮演不同角色的人或事物,如商品、账户、店铺等,使用浅绿色表示。
-
角色原型(Role Archetype,简称ROLE)
-
抽象了一种参与方式,由人或组织机构、地点或物品来承担,如客户、商家、财务组织等,使用浅黄色表示。
-
描述原型(Description Archetype,简称DESC)
-
对上述颜色表示的内容进行解释,用于分类或者描述建模过程中产生的数据,事件,或者活动,使用浅蓝色表示。
用一句话来概括四色原型就是:
一个什么样的人或物品以某种角色在某个时刻或某段时间内参与某个活动。其中“什么样的”就是DESC,“人或物品”就是PPT,”角色”就是ROLE,而“某个时刻或某个时间段内的某个活动”就是MI。
接下来使用四色建模法来分析领域模型,总共分为四大步:
- 建立时标原型:寻找需要追溯的事件,根据追溯事件寻找足迹
- 建立PPT原型:丰富模型,寻找时标原型周围的人/事/物,使它可以更好地描述业务概念
- 建立角色原型:进一步从中抽象出可以参与到不同流程中去的角色
- 建立描述原型:把一些信息用描述对象补足
案例:商家咨询
产品截图
梳理咨询业务时序
,任何的业务事件都会以某种数据的形式留下足迹。我们对于事件的追溯可以通过对数据的追溯来完成,当我们把这些数据的足迹按照时间顺序排列起来,就可以清晰的推测出过往的一段时间内发生的事情。
通过对业务的梳理可以产出了四色原型图的关键业务时刻,如下面表格
关键业务活动 | 关键业务时刻对象 | 备注 |
---|---|---|
注册到店app账号 | 用户账号信息 | |
建群 | 群关系记录 | 用户点击咨询会自动创建群 |
聊天 | 群消息 | 通过群聊实现不同客服的接待 |
打电话 | 电话记录 | 用户也可以通过打电话直接咨询商家 |
留资 | 客资信息 | |
视频面诊 | 面诊记录 |
通过上面的表格大概知道用户在IM应用中的关键业务时刻和对象
寻找关键业务时刻对象周围的人、事、物对象
从人,事,物中抽象出角色
把一些描述信息用对象补足
产出领域模型
限界笔纸法
限界笔纸法起源于thoughtworks,由thoughtworks大佬提出的基于四色建模的改进方法。在“四色建模法”的“时标对象”的基础上确定"限界上下文”与“聚集”的概念,再使用“纸和笔来管理”的方法,力图在建模过程中实现“分而治之”,增强数据的完整性,并避免过度设计。
建模步骤
- 根据“业务发生时刻”的价值识别核心领域(core domain)
- 确定核心领域之间的依赖关系
- 用纸和笔画表格并写实例(这里的实例可以是业务用例,用户故事,或者业务发生时刻)
- 确定“聚合根 (AGGREGATE ROOT)”
- 以“人以群分”的原则抽取新的“聚合”
分析模式
学会了上面的建模方法就有能力应对开发中等难度的系统了。不过,如果遇到更复杂的业务领域,还需要更深入的建模技能。《分析模式》是 Martin Fowler 写的一本领域建模的专著。讲述各种分析模式和辅助模式,专注于面向对象分析与设计的结果——模型本身,给出了来自金融贸易、测量、财务以及组织关系等多个领域内的一系列模式。书中每个模式都包含了设计背后的原理、使用的规则以及实现的技巧,给出的例子包含了有用模型的细节,并介绍了用于提高分析、建模和实现的重用技巧。研究不深感兴趣的可以看下。
代码建模
经过领域模型的分析后,面向对象已经初具雏形,但领域类并不能指导我们进行编码工作,因为领域类只是从用例模型中提炼出来的反映业务领域的概念,并不是真正意义上的软件类,我们要需要在进一步,完成领域类到软件类的转换,这就是代码建模阶段的主要任务。那么具体如何做的
-
第一步:完成领域类到代码类的映射
-
类:领域模型中的类直接映射成代码里的类
-
属性:领域模型类的属性直接映射成代码类里的属性
-
动作:领域模型类的动作直接映射成代码类里的方法
-
状态机:领域模型里的状态映射为类的状态机
-
协作关系:用UML的类关系图重新刻画协作关系
以上面的一个领域模型为例
- 第二步:应用设计原则和设计模式。完成了第一步,类就出来了,属性和方法也有了,是不是就结束了呢,如果按照设计原则的衡量标准,会发现第一步只是最基本的要求,而不是一个好的设计。还以"用户购买商品"案例为例,支付的方式分为支付宝、微信、银行卡,运用设计模式后,代码模型变成如下:
数据建模
数据模型推导的来源是领域模型。一般原则,领域模型中的实体映射为数据库中的表;领域模型中的属性,映射成表中的字段,同时还要根据需求补充更多的字段。模型中的一个一对多关联,可以映射成一个外键字段,以及一个外键约束。但一般不会真的建立外键约束,而外键的逻辑关系还是存在的。可以用虚线箭头表示这种逻辑上的外键关系,称为虚拟外键。对于多对多关联,必须增加一个关联表,其中包括了两个实体表各自的主键。另外,关联上的多重性决定了外键字段的非空约束。
怎么判断模型好不好
- 领域模型是生活中模型的映射。在几十年前,我们没有计算机,但是我们仍然可以通过白纸黑字的方式完成我们的业务。换句话说,我们的业务系统只是把一个纸质的单据变成了电脑里存在数据库里的东西。也就是说,我们的模型,一定是在生活中有实际的对应的单据,而且名字也应该是一致的。千万不要臆造概念。
- 领域模型不是DB模型。领域模型不是DB模型,一个领域对象可以有多种方式映射到数据库,但是我们聊领域模型的时候,一定不是聊DB模型,重要的一定是描述清楚业务概念。在做ORM的时候,如果有继承关系需要的实现的时候,我们再考虑是单表继承(Single Table Inheritance)还是实体表(Concrete Table Inheritance)等模式。
- 领域模型的关系一定是稳定的。当接到一个新的需求时,如果发现模型的关系不匹配了,那说明之前的模型设计的肯定存在问题,好的模型实体和实体之间的关系一定是稳定的,只会新增不会对原有关系进行改变。
领域模型为什么要一直持续迭代
要在"变化的现实世界中寻找不变性",希望寻找到一个稳定的领域模型,让系统流程可以灵活改变,模型不怎么变,但在实际中却很难完美的做到,这是为什么呢?(备注:这里的迭代不是指对原有模型关系的重构,而是对模型新关系的升级)
- 意识问题。在用户、产品人员、运营人员眼中,沟通的语音是"流程"而不是"模型"。开发人员在与他们的沟通过程中,慢慢就形成了以"流程"为主导,而不是以"模型"为主导的思维方式。这使得整个开发过程是"流程驱动",而不是"领域驱动"。大家在讨论业务与系统解决方案的时候,大部分时间都花在了业务流程、业务规则上,而不是深刻挖掘流程背后的不变因素。
- 现实世界的复杂性。业务也就是我们的现实世界,灰度的、模棱两可的东西,比计算机的世界多得多,变化也多得多。很难确定有哪些东西是不怎么变的,什么东西是容易变的,而这恰恰是做建模的前提条件。
- 迭代速度。在稳固的模型,也不可能一成不变,毕竟现实世界一直在变。当现实世界变化到模型不能支撑的时候,要能马上修改模型才行。但实际情况是,因为开发效率的原因,工期赶不上,然后就会在旧的模型上进行打补丁,补丁一个接着一个打,最后整个系统臃肿不堪,开发效率进一步降低,如此恶性循环。
- 火候的掌握。领域模型是要对现实世界建模,既要去寻找不变性,又要为可能变化的地方留出扩展性。什么地方是不变的,要作为基础;什么地方是易变的,要留出扩展性,这其中并没有一个标准原则。另外,各家公司的业务规模、速度不一样,团队实施能力也不一样。所以在实践中,要么会"缺乏设计",要么会"过度设计"。对火候的掌握,需要有悟性。只有反复思考,反复推翻自己之前的想法,再重建新的想法,才能在实践中不断找到领域模型、业务发展速度、技术团队能力之间的"最佳平衡点"。
背后需要修炼的思维
上面讲的这些都是术的表现,是借假修真,只有"真"才是我们修炼的道,也就是做事的思维,下图列举一些,欢迎感兴趣的多交流。