安然写字的地方

DDD相关笔记

领域驱动设计

领域驱动本身是针对复杂系统设计的软件工程方法,它在战略层面有三个重要的点,一是聚焦业务核心价值,二是统一语言,三是业务领域性划分。

聚焦业务核心价值,也是领域驱动战略层面最主要的目标;统一语言是基于领域划分的,也就是在一个领域限界上下文中统一语言。通常情况下在微服务会对应到一个限界上下文。在战术层目标主要是在设计系统时,需要具备高内聚、低耦合、易扩展、易维护四个要点。战术层面主要是为了保证空泛的战略的无法落地,为落地提供了一系列具体的开发模式。这也是代码即架构的一种体现。

如何识别聚合与聚合根?

明确含义:一个BoundedContext(界定的上下文)可能包含多个聚合,每个聚合都有一个根实体,叫做聚合根;

识别顺序:先找出哪些实体可能是聚合根,再逐个分析每个聚合根的边界,即该聚合根应该聚合哪些实体或值对象;最后再划分BoundedContext;

聚合边界确定法则:根据不变性约束规则(Invariant)。不变性规则有两类: 1)聚合边界内必须具有哪些信息,如果没有这些信息就不能称为一个有效的聚合; 2)聚合内的某些对象的状态必须满足某个业务规则;

例子分析1:订单模型

Order(一个订单) -必须有对应的客户信息,否则就不能称为一个有效的Order; -同理,Order对OrderLineItem有不变性约束,Order也必须至少有一个OrderLineItem(一条订单明细),否则就不能称为一个有效的Order; -另外,Order中的任何OrderLineItem的数量都不能为0,否则认为该OrderLineItem是无效的,同时可以推理出Order也可能是无效的。因为如果允许一个OrderLineItem的数量为0的话,就意味着可能会出现所有OrderLineItem的数量都为0,这就导致整个Order的总价为0,这是没有任何意义的,是不允许的,从而导致Order无效; 所以,必须要求Order中所有的OrderLineItem的数量都不能为0; 那么现在可以确定的是Order必须包含一些OrderLineItem,那么应该是通过引用的方式还是ID关联的方式来表达这种包含关系呢?

这就需要引出另外一个问题,那就是先要分析出是OrderLineItem是否是一个独立的聚合根。 回答了这个问题,那么根据上面的规则就知道应该用对象引用还是用ID关联了。那么OrderLineItem是否是一个独立的聚合根呢? 因为聚合根意味着是某个聚合的根,而聚合有代表着某个上下文边界,而一个上下文边界又代表着某个独立的业务场景,这个业务场景操作的唯一对象总是该上下文边界内的聚合根。想到这里,我们就可以想想,有没有什么场景是会绕开订单直接对某个订单明细进行操作的。也就是在这种情况下,我们是以OrderLineItem为主体,完全是在面向OrderLineItem在做业务操作。有这种业务场景吗?没有,我们对OrderLineItem的所有的操作都是以Order为出发点,我们总是会面向整个Order在做业务操作,比如向Order中增加明细,修改Order的某个明细对应的商品的购买数量,从Order中移除某个明细,等等类似操作,我们从来不会从OrderlineItem为出发点去执行一些业务操作;另外,从生命周期的角度去理解,那么OrderLineItem离开Order没有任何存在的意义,也就是说OrderLineItem的生命周期是从属于Order的。所以,我们可以很确信的回答,OrderLineItem是一个实体。

例子分析2:帖子与回复的模型,做个对比,以便更好地理解。

不变性分析:帖子和回复之间有不变性规则吗?似乎我们只知道一点是肯定的,那就是帖子和回复之间的关系,1:N的关系;除了这个之外,我们看不到任何其他的不变性规则。那么这个1:N的对象关系是一种不变性规则吗?不是!首先,一个帖子可以没有任何回复,帖子也不对它的回复有任何规则约束,它甚至都不知道自己有多少个回复;再次,发表了一个回复和帖子也没有任何关系;其次,发表回复对帖子没有任何改变;从业务场景的角度去分析,我们有发表帖子的场景,有发表回复的场景。当在发表回复的时候,是以回复为主体的,帖子只是这个回复里所包含的必要信息,用于说明这个回复是对哪个帖子的回复。这些都说明帖子和回复之间找不出任何不变性约束的规则;因为帖子和回复都有各自独立的业务场景的需要,所以可以很容易理解它们都是独立的聚合根;那也很容易知道该如何建立他们之间的关联了,但是我们要尽量减少关联,所以只保留回复对帖子的关联即可;帖子没有任何必要去保存一个回复的ID的列表;那么你可能会说,当我删除一个帖子后,回复应该是没有存在的意义的呀?不对,不是没有存在的意义,而是删除了帖子后导致了回复对帖子的关联信息的缺失,导致数据不一致。这是因为帖子和回复之间有一种必然的联系(1:N),回复一定会有一个对应的帖子;但是回复有其自己的生命周期,不应该随着帖子的删除而级联删除。这种情况下,如果你删除了帖子,就导致回复也成为了一条无效的数据;所以,我们绝对不允许删除任何聚合根,因为一旦你删除了聚合根,那就意味着与该聚合根相关的其他任何聚合根都会有外键引用缺失的问题,会导致整个领域模型数据的不一致;所以,永远都不要删除聚合根;

例子分析3:博客评论模型

拿博客评论举个例子:比如3个Entity:

User Blog CommentOfBlog 可以理解为一个User发布了若干Blog,一个Blog又有若干CommentOfBlog,所以User就可以认为是聚会根了吗? 其实不是的,如果这样去思考,那整个系统只需要User一个聚合根就可以了,因为所有的东西都是User录入的。 我实际上也没否定聚合或聚合根的概念。我只是觉得聚合的概念过去单一,实际上我们对象之间的关系有很多种,有弱关联,聚合,组合。我现在更多思考的是一个对象是否可以独立?然后会从两个相关对象的各自的角度进行分别思考,最后得出对象应该独立还是从属于另外一个对象; User一定可以独立; Blog呢?首先User和Blog是一对多的关系,User可以发表多篇blog,但这不表明user离开blog就不能活了,或者说是否user离开blog就没意义了?显然不是。因此可以推理出user不需要聚合blog; 从blog的角度分析,虽然每个blog都有一个作者(author,user类的一个实例),但是blog存在的意义是否总是为了user而存在,我们创建blog的目的是为了被内聚到user吗?显然不是,blog创建的目的与user无关,我们是为了写随笔而创建blog,我们创建一个blog不是为了在user里增加一个blog。所以,应该理解为blog.Author,即author只是一个blog的关联属性,表示谁创建了该blog。 CommentOfBlog:一个blog可以有多个comment,从blog角度分析,blog是否必须聚合comment?不见得,因为blog可以没有评论;但是从comment的角度来看,comment是否总为了blog而存在?实际上是的, 1)comment可以属于其他的Blog吗?不可以; 2)comment被创建的目的是为了评论blog,可以说离开blog谈comment总是完全没有任何意义。 所以我们会让blog聚合comment。之前我们分析聚合,可能更多的仅仅总是从一方面去分析,就是会分析当前聚合根应该聚合哪些对象,而不会从哪些被聚合的实体上去分析它是否真的应该被聚合还是应该独立。所以有时的出来的聚合会不准确。

总结一下

  1. 找最小的业务场景
  2. 找出聚合根(先确定可能是聚合根的实体,可能包含的实体),确定的原则是:具有独立的生命周期(生命开始和结束)
  3. 确定聚合根的边界,如包括Entity1,E2,E3.。。。这些实体要依赖于聚合根的存在而存在。 聚合边界确定法则:根据不变性约束规则(Invariant)。

不变性规则有两类:

  1. 聚合边界内必须具有哪些信息,如果没有这些信息就不能称为一个有效的聚合;
  2. 聚合内的某些对象的状态必须满足某个业务规则

反向验证:

  1. 验证一个实体是否属于这个聚合:他还可以属于其他聚合吗?离开了这个聚合根是不是就失去了意义。
  2. 实体变化了,影响聚合根的内容了吗?

再次验证:

  1. 是否“同生死,共存亡”; 聚合除了封装我们关心的信息外最主要的目的就是为了封装业务规则,保证数据的一致性, 业务规则比如一个银行账号的余额不能小于0,订单中的订单明细的个数不能为0,订单中不能出现两个明细对应的商品ID相同,订单明细中的商品信息必须合法,商品的名称不能为空,回复被创建时必须要传入被回复的帖子(因为没有帖子的回复不是一个合法的回复)等

聚合应当设计的尽可能小 聚合包含的东西过多,导致多人操作时并发冲突严重,导致系统可用性变差;所以实现了既能解决并发冲突的问题,也能保证让聚合来封装业务规则,实现模型级别的数据一致性;另外,聚合设计的小还有一个好处,就是:业务决定聚合,业务改变聚合。聚合设计的小除了可以降低并发冲突的可能性之外,同样减少了业务改变的时候,聚合的拆分个数,降低了聚合大幅重构(拆分)的可能性,从而能让我们的领域模型更能适应业务的变化。

聚合之间通过ID关联 其实聚合之间无需通过对象引用的方式来关联;

需要关联时,必须要

  1. 尽量避免多对多的关系;
  2. 如果必须多对多,应该转换为两个一对多,然后都只要在双方对象中保存对方的ID即可;

我觉得聚合的设计主要把握以下几点吧: 1)强调Invariants,即不变性; 2)并不是A因为B才有存在的意义就一定表示A被B聚合,此时A也可能是聚合根,帖子和回复的例子就说明了这一点; 3)如何识别聚合确实还是应该从业务上分析,而不是从技术角度去左右;应该从业务上分析实体之间的关系,找出真正的不变性,从而确定聚合的边界; 4)聚合之间通过ID关联有太多好处,特别是在分布式环境时更加凸显,具体好处需要你自己开发时才会感受到,想想什么是数据,什么是行为类吧,为什么数据与数据之间几乎没什么多态,而框架类库中的那些类能很好的通过对象引用实现多态?其实ID关联表示A明确知道它关联了哪个B,而对象引用表示A拥有某个类型的B,但是从语义上来说它不知道引用的是哪个B。从这个角度去想想为什么领域模型中的对象类之间为什么要用ID关联,而框架类库中强调行为的类为什么是对象引用吧。至于其他技术方面的好处,太多了,自己慢慢体会吧,呵呵;想想CQRS架构为什么也采用ID关联吧,想想为什么Evans也在其官网上推荐ID关联? 5)至于上面一个朋友提到的关于领域模型中对象与对象之间的交互上,即一个对象做了什么改变后另一个对象会做出相应的修改,这个有两种方法:1)DomainService;2)Messaging;从抽象的角度来说,DomainService是一种过程化思维,先做什么后做什么都在DomainService中完成;这也是经典的DDD的做法,用于处理一个领域逻辑需要跨聚合根的情况;2)如果用Messaging,典型的架构是CQRS,Messaging就是通过发送消息和接受消息实现两个聚合之间的通信,一般通过bus实现publish-subscribe的消息模式;通过这种方式的好处是,把原来的getandcall模式转换为publish-subscribe模式,这样的转换带来的好处就是可以灵活配置需要同步订阅消息还是异步订阅消息,以及对象之间不再依赖;我们可以根据业务的数据一致性要求来决定采用同步还是异步订阅消息;异步分布式消息通信的知识可以去学习下NServiceBus。

关于如何区分对象和服务

对象对应领域中的一些名词性的概念;服务对应领域中的一些动词性的概念。如货物,书本是对象,我们在领域中需要唯一区分这些对象;领域服务用来表示一个涉及到多个领域对象的一个动作或转换,如资金转帐就是一个领域服务,因为它涉及到两个银行帐号领域对象;之所以没有把领域服务看成是对象是因为 1)它和一般的对象不同,一般的对象既有状态也有行为,而领域服务只有行为没有状态,如果一个概念没有状态只有行为,那么我们是没有必要把它看成是对象的,因为永远不需要持久化它或重建它。对于领域服务,我们只要关心它能做什么,不用关心它的生命周期,所以它在整个领域模型中的任何时候都只要一个实例即可。 2)对于图书借还系统而言,借书者是一个对象,因为我们需要区分是哪个借书者;但是图书馆在我的场景中只需要一个,所有的借书者都面对同一个图书馆进行交互;可以认为图书馆提供了让用户借书和还书的服务。所以,我把图书馆定义为一个服务; 3)至于你说的如果有很多大学,它们分别都有自己的图书馆,那就是不同的场景了。此时我觉得图书馆就是一个对象,因为一个借书者可能会区分是从这个图书馆借的书还是那个图书馆借的书。

一致性

谈谈一致性这个名词 关于Paxos说的一致性,个人理解是指冗余副本(或状态等,但都是因为存在冗余)的一致性。这与关系型数据库中ACID的一致性说的不是一个东西。在关系数据库里,可以连副本都没有,何谈副本的一致性?按照经典定义,ACID中的C指的是在一个事务中,事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。那么,什么又是一致性状态呢,这跟业务约束有关系,比如经典的转账事务,事务处理完毕后,不能出现一个账户钱被扣了,另一个账户的钱没有增加的情况,如果两者加起来的钱还是等于转账前的钱,那么就是一致性状态。

从很多博文来看,对这两种一致性往往混淆起来。另外,CAP原则里面所说的一致性,个人认为是指副本一致性,与Paxos里面的一致性接近。都是处理“因为冗余数据的存在而需要保证多个副本保持一致”的问题,NoSQL放弃的强一致性也是指副本一致性,最终一致性也是指副本达到完全相同存在一定延时。

当然,如果数据库本身是分布式的,且存在冗余副本,则除了解决事务在业务逻辑上的一致性问题外,同时需要解决副本一致性问题,此时可以利用Paxos协议。但解决了副本一致性问题,还不能完全解决业务逻辑一致性;如果是分布式数据库,但并不存在副本的情况,事务的一致性需要根据业务约束进行设计。

另外,谈到Paxos时,还会涉及到拜占庭将军问题,它指的是在存在消息丢失的不可靠信道上试图通过消息传递的方式达到一致性是不可能的。Paxos本身就是利用消息传递方式解决一致性问题的,所以它的假定是信道必须可靠,这里的可靠,主要指消息不会被篡改。消息丢失是允许的。