战术设计
战术是对限定上下文进行详细设计,进一步讲就是对限界上下文中的模型按业务规则拆分为实体、值对象以及通过对模型操作(领域事件)识别出来的聚合实体。本节重点是模型的拆分,对于域对象的细节会在后续章节详细展开。先了解下相关的概念。
关于领域:领域是一个概念,如果细分的话可以分为:1、模型,主要包含实体、值、对象、资源库等;2、操作,主要包含领域服务和领域事件。在后续章节我们会用具体的术语来代替模型。 |
---|
术语
- 实体(Entry):具有连续性和标识,在数据互换以及分布式的环境下要保持这种标识符的唯一性,一般只关心这个唯一标识并不关心过多的属性值;
- 值对象(Value Object):描述事务的某种状态,没有概念标识。一般关心属性值并不关心VO是什么;
- 聚合(Aggregate):可以简单理解为实体和值对象的集合,本节后续会详细说明聚合的概念;
- 领域事件(Domain Event):记录限界上下文中发生的对业务产生重要影响的事情。设计时需要关注其并发的一致性问题;
上述这几个领域模型的关系如下:
实施步骤
第一步、识别实体和值对象
简单来说实体和值对象可以通过是否需要有唯一标识来识别,比如在购物场景中,用户和收货地址,用户一般包含用户名、密码、身份证号等属性,地址一般由省、市、县、街道门牌号等多个属性组成。在这个例子中我们适合把用户设计成一个实体,因为在实际场景中用户要做到全局唯一,这个唯一标识可以是身份证号也可以是用户英文名,而地址的唯一性是由多个属性共同组成,从业务角度来讲,同一地址是可以被多个用户使用的所以适合设计成一个值对象,被用户关联即可,即使在同一限界上下文中也没必要设置其唯一性,实体和值对象详细区别如下:
实体 | 值对象 |
---|---|
具有生命周期有唯一标识通过ID判断相等性有增删改查方法可变例如:汽车、订单 | 用完即扔,起描述性作用没有唯一标识通过属性判断相等性实现equals和hashcode方法不可变例如:颜色、地址 |
在协同系统中,优先级表现为一个标识,在某种程度上来说它是可以重复的,只需用描述字段定义其含义即可,所以我们的实体和值对象如下所示,在图上我们会有首字母来表示不同的领域类型,其中V=value object, E=entry, A=Aggregate。
第二步、识别聚合
在DDD落地时一定会存在实体和值对象,但可以不使用聚合,那么为什么要用聚合呢?我们用一个类比来理解一下:宇宙由一些永恒的物体聚合而成(实体和值对象),这些物体通过某种因果关系联系在一起,这种关系独立于物体本身(强调业务),并且存在于客观的空间和时间中(会随着认知的变化而变化)。翻译过来就是同一业务模型在不同场景时其聚合对象可能不一样,甚至会存在同一业务模型多套聚合这样的实现(但不在极端情况下不建议这样来设计,建议用领域服务或应用服务来完成这种设计),先参考下图来了解下聚合的识别过程,实施时可参考下面4条规则:
规则一:聚合内在聚合边界内保护业务规则不变性
这条规则强调了聚合的设计范围。了解了模型中真正的不变条件,才能决定什么样的对象可以放在一个聚合中,不变指的是业务规则的确定性;聚合的组成部分应该由业务最终决定,而且要以一次事务提交中必须保持一致的内容为基础。
分析时可以遵循此规则基于粗粒度的业务模型关联做删减;在本例中,为了待办项,需要拆解为多个子任务,任务必须属于确定的待办项,任务不能脱离开待办项独立存在,所以当待办项被废弃后,与之关联的所有任务也就失去了存在的意义,如下图:
规则二:聚合内聚合要设计的小巧
这条规则表现为CAP之间的权衡,系统建模时大的聚合会比较占资源也不利于性能、伸缩,在设计时尽量遵循一个聚合只有一个聚合根,一个事务中只修改一个聚合实例这样的最小化原则。以下是此条规则建议的实施步骤:
- 开始设计时一个聚合根只包含一个实体,DDD中称为聚合根;
- 填充与此聚合根关系最紧密的字段到聚合根类别,同时需要注意业务场景,比如在一些场景下,描述(desc)是一定会和主数据同时修改的,但可能会存储在不同的数据表中,这也是笔者一直强调的要基于业务模型而不能基于数据模型的目的之一;
- 基于已有字段做一致性检查,检查其它聚合是否有关联响应关系,并为每个影响加上业务上的影响时间(实时、延时或是具体数字);
- 如果响应时间为实时,在这个聚合根中加入另一个实体;如果非实时,则可以设计领域事件来设计实体间的关联响应关系;
- 设计测试用例,用来佐证模型设计是否太复杂,如果设计出来的聚合不能被测试可以复查一下;
在上述例子中,Product是一个聚合根,实际上可能会有两种建模方案,左侧比较符合业务上的定义,但这样的聚合会占用大量的内存,右侧为经过小巧化后的模型方案,至于采用哪种没有统一的标准,需要设计者自己来权衡。
规则三:聚合间只能通过标识符引用其它聚合
采用小巧化后的方案原来的一个聚合裂变成了四个聚合,但我们不能破坏业务的完整性,所以在落地时就会涉及到引用问题,采用实体引用还是采用标识引用,下面几点仅供参考:
- 采用标识引用,可以实现在多个事务中进行修改;
- 采用对象引用会破坏事务一致性原则;
- 采用标识引用可以解决性能问题,但同时也引入了对象导航问题;
- 落地时需要考虑竞争和时间复杂度两个因素。
在采用标识引用时可能会导致:1、失联领域模型;2、N 1查询问题;设计者需要做下权衡;经过分析后,协同系统的模型如下图所示:
规则四:聚合间利用最终一致性更新其他聚合
初看貌似和第一条规则有冲突,实际上并没不是这样。第一条规则强调的是业务的一致性,此条实际上是对第一条的补充。如果聚合间是在同一线程中运行建议采用强一致性设计,如果在不同的线程中,建议采用最终一致性设计;
特殊情况下,如果业务规则对实时性要求较高,线程间也可以选择分布式事务机制来达到强一致性的目的。简单点理解的话这是一个技术上的选择,采用强一致性还是最终一致性来实现系统;识别聚合后的协同系统模型如下图所示:
打破原则的理由以上四条规则实际上只是经验的总结,是经过验证过的不成文规范。出现下列情况时,我们就需要在遵循规则的前提下做出权衡了,比如:端应用:比如业务关联非常复杂,对聚合根进行更新操作时需要关联多个实体变更,但用户界面又非常依赖这些关联变更;这时可能需要在多个聚合强一致性更新还是聚合大小上做出选择了;缺乏技术机制:缺少必要的中间件支撑;全局事务:尽量不要全局事务,因为会影响系统的伸缩性,在设计时尽量避免这样的设计;业务发生变化:原来的聚合已不适应当前的业务需求了,是选择修改聚合还是用原来的实体新建一个聚合,就可能需要综合业务和架构重新权衡 |
---|
- 端应用:比如业务关联非常复杂,对聚合根进行更新操作时需要关联多个实体变更,但用户界面又非常依赖这些关联变更;这时可能需要在多个聚合强一致性更新还是聚合大小上做出选择了;
- 缺乏技术机制:缺少必要的中间件支撑;
- 全局事务:尽量不要全局事务,因为会影响系统的伸缩性,在设计时尽量避免这样的设计;
- 业务发生变化:原来的聚合已不适应当前的业务需求了,是选择修改聚合还是用原来的实体新建一个聚合,就可能需要综合业务和架构重新权衡
第三步、识别领域事件
领域事件是领域模型的一部分,用来维护事务的一致性。从实用角度来讲领域事件只适合那些需要最终一致性的业务场景而不适合强一致性的事务业务场景。领域事件可由聚合或领域服务发布并被领域服务消费。
简单理解,就是当某件事发生后,需要通知相关干系方做出相应的响应。在协同系统中当一个BacklogItem被删除了,就可以发出一个领域事件,通知Task做删除操作。事件体系非常复杂,本节中不详细展开,我们会在专门的章节讲解,下图是经过删减后的事件过程,读者可简单了解一下。其实笔者并不建议在DDD实践时使用事件这种架构设计。
不建议使用领域事件 首先在应用中使用事件设计必然会影响性能,在同一线程通常采用springEvent机制实现必须会占用资源,跨线程时通常会采用message框架必须会影响响应时间;其次事件的构建和控制顺序性非常复杂,还有更复杂的快照、事件源等设计。笔者会在架构设计和A ES设计章节时详细阐述这方面的内容。 软件架构设计的宗旨是在满足业务的前提下尽量简单,而不是为了彰显技术,从而使简单的系统人为复杂化。笔者就曾经见过在同一应用采用mq相互调用的设计,团队成员甚至维护了一份很完善的topic映射表,笔者咨询过其设计人员,答复是为了解耦。后来闲暇时笔者从业务和SLA等方面研究过那个应用,很多地方是完全可以改用Interface方式调用的,另外的一小部分也比较适合分离到另外一个应用中做为基础服务进而达到共享的目的。这个例子并不是说这种设计不好,从支撑业务角度来讲是个合格的设计,但在笔者看来还是有改进空间的,还可以更优雅一些。 所以建议我们的架构师同学们学习也好,设计系统也罢一定要追求其设计内涵,而不要追求其外在的形,比如MVC架构我们换一种画法,把V和C画成一个圆然后把M包在中间,就可以说成是六边形架构。设计人员的一时痛快导致的设计偏差埋下的坑在日后是需要众多兄弟们没日没夜的加班来填的,即然不能保证完美的设计,但可以做到在迭代中演进,这也是笔者在上面章节中所阐述的架构师一定要多参与日常需求开发的重要原因之一。 |
---|
经过分析后,我们最终的模型可能是下面的样子,同样需要把新增的内容补充到通用语言表中:
战术设计结果
输出物 | 类型 | 说明 |
---|---|---|
上下文映射图 | 图型 | 见节:第四步、上下文映射图或第五步、补充上下文映射图 |
通用语言描述表 | Excel表格 | 见节:第二页、提炼精炼语言中的表格,补充本节的通用语言到这个表中 |
模型图 | 图形 | 见节:识别领域事件,第二张图 |