聚合和聚合根:怎样设计聚合
在构建领域模型时,我们会根据用户旅程或场景分析中的一些业务操作和行为找出产生这些行为的实体或值对象,从这些实体对象中找出聚合根,进而将这些业务关联紧密的聚合根、实体和值对象组合在一起,构成聚合,再根据业务语义边界将多个聚合划定到同一个限界上下文中,在限界上下文内构建领域模型。
聚合
在DDD中,实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有相对丰富的业务属性和业务行为。而值对象主要是属性集合,主要完成对实体的状态和特征描述。
但实体和值对象都只是个体化的业务对象,它们所表现出来的是个体的行为和能力。在领域模型中我们需要一个这样的组织,将这些紧密关联的个体对象聚集在一起,按照组织内统一的业务规则共同完成特定的业务功能,因此就有了聚合的概念。
那聚合在这个组织中到底起到什么样的作用呢?
举个例子。社会是由一个个的个体组成的,象征着我们每一个人。随着社会的发展,慢慢就出现了社团、机构、部门等组织,个人可以加入这些组织,于是我们就从个人变成了组织的一员,大家在组织内按照组织的章程和目标,协同一致地工作,进而发挥出更大的价值和力量。
领域模型内的实体和值对象就类似这些组织中的个体,而能让实体和值对象协同工作的组织就是聚合。聚合内部有自己的业务规则,类似社团组织中的章程,所有成员必须遵守。有了这些业务规则就可以确保聚合内的这些领域对象,在实现聚合内的业务逻辑时,可以保证数据的一致性。比如,订单聚合就有自己内部的业务规则,在订单聚合内每次修改商品数据时,它们都必须符合订单聚合的业务规则:"订单总金额等于所有商品明细金额之和。"违反了这个业务规则,就会出现聚合数据不一致等诸多问题。
从技术的角度,你可以这么理解,聚合是由业务和逻辑紧密关联的实体和值对象组合而成的。聚合内数据的修改必须由聚合根统一组织,以确保每次数据修改都是按照聚合内统一的业务规则来完成,聚合是数据修改和持久化的基本单元。过去,在传统数据模型中每一个实体都是对等的,在业务逻辑实现时,可以随意找到实体或数据库表完成数据修改,但这类操作在DDD的聚合内是不被允许的!
在聚合中有一个聚合根和上下文边界,但这个边界比限界上下文的边界小,它主要是根据业务的单一职责和高内聚设计原则,定义了聚合内部应该包含哪些实体和值对象。聚合之间的边界是松耦合的,多个聚合在同一个限界上下文和微服务内。按照这种方式设计出来的微服务,很自然就符合"高内聚,低耦合"的设计要求了。
聚合在DDD分层架构里属于领域层,同一个微服务的领域层可以有多个聚合,每个聚合内有一个聚合根,多个实体、值对象和领域服务等领域对象。同一个限界上下文内的多个聚合,通过应用层组合在一起共同实现了领域模型的核心领域逻辑。
我们为每一个聚合设计一个仓储完成聚合数据的持久化操作。为了避免聚合数据频繁地提交,建议你尽可能将聚合内变更的数据,封装在一次交易中提交仓储完成持久化。
聚合在领域模型里是一个逻辑边界,它本身没有业务逻辑实现相关的代码。聚合的业务逻辑是由聚合内的聚合根、实体、值对象和领域服务等来实现的。聚合内的实体以充血模型实现自身的业务逻辑。跨多个实体的领域逻辑通过领域服务来实现。比如,有的业务场景需要同一个聚合的A和B两个实体来共同完成,我们就可以将这段业务逻辑用领域服务组合A和B两个实体来完成。
跨多个聚合的业务逻辑的组合和编排,是通过应用服务来实现的。比如,有的业务逻辑需要聚合C和聚合D中的两个领域服务来共同完成,为了避免聚合之间的领域服务直接调用,实现微服务内聚合解耦,此时你可以将这段业务逻辑上升到应用层,通过应用服务组合两个聚合的领域服务来实现。
聚合根
聚合内有一定的业务规则以确保聚合内数据的一致性,如果在实现业务逻辑时,任由服务对聚合内实体数据进行修改,那么很可能会因为在数据变更过程中失去统一的业务规则控制,而导致聚合内实体之间数据逻辑的不一致。而如果采用锁的方式则会增加软件的复杂度,也会降低系统的性能。
聚合根的主要目的是避免聚合内由于复杂数据模型缺少统一的业务规则控制,而导致聚合内实体和值对象等领域对象之间数据不一致性的问题。也就是说,在聚合根的方法或领域服务中可以用上这些业务规则,来确保聚合内数据变更时可以保持数据逻辑的一致性。
如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,但它不仅是实体,还是聚合的管理者。
▪首先,聚合根是实体,作为实体,它拥有实体的业务属性和业务行为,可以在聚合根实现自身的业务逻辑。
▪其次,它作为聚合的管理者,在聚合内负责协调实体和值对象,按照固定的业务规则,协同完成聚合共同的业务逻辑。
▪最后,它还是聚合对外的联络人和接口人,聚合之间以聚合根ID关联的方式接受聚合的外部任务和请求,在限界上下文内实现聚合之间的业务协同,聚合外部对象不能直接通过对象引用的方式访问聚合内的对象。比如,当你需要访问其他聚合的实体时,你可以在应用服务中调用其他聚合的领域服务,将关联的聚合根ID作为服务参数,先访问聚合根,再通过聚合根导航到聚合内部实体。
聚合根管理了聚合内所有实体和值对象的生命周期,我们通过聚合根就可以获取到聚合内所有实体和值对象等领域对象。一般来说,如果聚合根被删除了,那么被它引用的实体和值对象也就不会存在了。
在聚合根类的方法中,可以组织聚合内部的领域对象,完成跨多个实体的复杂业务逻辑。但是,在聚合的领域服务中,也可以完成跨多个实体的复杂领域逻辑。
你可能会问,那跨多个实体的业务逻辑到底应该在聚合根方法还是在领域服务中实现呢?
理论上,聚合根方法和领域服务都可以组合多个实体对象完成复杂的领域逻辑。但为了避免聚合根的业务逻辑过于复杂,避免聚合根类代码量过于庞大,我个人建议聚合根除了承担它的聚合管理职能外,只作为实体实现与聚合根自身行为相关的业务逻辑,而将跨多个实体的复杂领域逻辑统一放在领域服务中实现。当然,简单聚合的跨多个实体的领域逻辑,可以考虑在聚合根的方法中实现。
大部分富领域模型的业务领域,在领域建模的过程中,都可以找到聚合根,建立聚合,划分限界上下文,建立领域模型。但也有部分贫领域模型的场景,比如数据计算、统计以及批处理等业务场景,这些实体都是平等、独立且无依赖的,在领域建模时你找不到聚合根。但这些实体对象的业务依赖却非常紧密,在业务上是高内聚的。我们不妨也将这些业务关联紧密的实体集合作为一个聚合处理,除了不考虑聚合根外,其他诸如DDD分层架构等设计方法都是可以借鉴的。
聚合的设计步骤
聚合作为领域模型中重要的业务功能单元,它的设计是领域建模过程中非常重要的工作。DDD领域建模时通常采用事件风暴方法,采用用户旅程分析和场景分析等需求分析方法,针对特定的业务场景梳理出所有业务行为和领域事件,然后找出所有产生这些业务行为的实体和值对象等领域对象,梳理这些领域对象之间的关系,找出聚合根,找出与聚合根业务紧密关联的实体和值对象,组合并构建聚合。
下面我们以保险的投保业务场景为例,看一看在聚合构建过程中有哪些关键步骤。
第1步,采用事件风暴,梳理投保业务场景中发生的所有业务行为,找出产生这些行为的所有实体和值对象,比如投保单、标的、客户、被保人等。
第2步,从众多实体中找出适合作为聚合对象管理者的根实体,也就是聚合根。判断一个实体是否是聚合根,你可以结合以下内容进行分析。是否有独立的生命周期?是否有全局唯一ID?是否可以创建或修改其他对象?是否有专门的模块来管理这个实体?
第3步,根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象,构建出一个包含聚合根(唯一)、多个实体和值对象的领域对象的集合,这个集合就是聚合。
第4步,在聚合内根据聚合根、实体和值对象的依赖关系,找出它们的引用和依赖关系。
这里需要说明一下:投保聚合有投保人和被保人两个值对象,它们的数据来源于客户聚合。客户聚合有客户聚合根,在这里对客户信息进行管理,完成客户数据的增、删、改、查等操作。投保人和被保人两个值对象数据是客户数据的冗余数据,记录投保那一刻投保人和被保人的客户快照数据。即使未来某一天客户聚合的数据发生了变更,也不会影响投保单中投保人和被保人值对象的快照数据。如果希望得到它们的最新数据,你也可以通过关联客户ID从客户聚合中查询后获取。
我们还可以看出实体之间的引用和依赖关系,比如在投保聚合里投保单聚合根引用了报价单实体,报价单实体则引用了报价规则值对象。
第5步,多个聚合根据业务语义和上下文边界,划分到同一个限界上下文内,就完成了领域建模,这时聚合的构建过程也结束了。
这就是一个聚合诞生的完整过程。
聚合的设计原则
DDD中有很多设计原则,聚合作为领域模型的核心业务逻辑单元,"高内聚,松耦合"的执行者和业务规则的遵循者,当然少不了设计原则。
我们不妨先看一下《实现领域驱动设计》一书中对聚合设计原则的描述,原文理解起来可能有点困难,这里逐一解释一下。
[第一条,在一致性边界内建模真正的不变条件。] 聚合是用来封装真正的业务不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,保证聚合内对象数据的一致性。比如,订单聚合的核心业务规则就是:订单总金额等于所有商品明细金额之和。
聚合边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因,所以除了限界上下文,聚合是可以拆分为微服务的最小业务单元。
但需要记住:除非非常必要,不建议按照聚合对微服务过度拆分。
[第二条,设计小聚合。] 如果聚合设计得过大,聚合会因为包含过多的实体和值对象,导致实体之间的管理过于复杂,以及领域逻辑实现复杂,在高频操作时就可能会出现并发冲突或者数据库锁,最终导致系统可用性变差。小聚合设计可以降低由于业务过大,在业务变化时导致聚合重构的可能性。这样领域模型就更能适应业务的变化。
[第三条,通过唯一标识引用其他聚合。] 聚合之间是通过引用聚合根ID的方式,而不是通过直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。
为什么聚合根之间不采用对象引用的方式呢?
其实这样设计也是为了聚合的解耦。当领域模型随着业务需求发生变化,微服务内需要进行聚合拆分时,原来领域模型和微服务内聚合根之间对象引用的方式,就会变成跨微服务的调用,跨微服务后这种对象引用就会失效,在微服务架构演进时就需要比较大的代码调整。
采用聚合根ID引用的方式,则可以将聚合根ID作为服务参数,进行跨聚合的领域服务调用。在微服务拆分时,如果聚合被分别拆分到两个不同的微服务中,这种代码的改动量就会小很多,聚合的边界也会清晰很多。
[第四条,在边界之外使用最终一致性。] 在聚合内采用数据强一致性,在聚合之间采用数据最终一致性。这是因为DDD强调在一次事务中,最多只能修改一个聚合的数据。如果一次业务交易操作涉及了多个聚合数据的修改,那么应采用领域事件驱动机制,通过数据最终一致性异步更新所有聚合的数据,从而实现聚合解耦。我会在第9章详细讲解领域事件驱动实现机制。
[第五条,通过应用层实现跨聚合的服务调用。] 聚合是领域层的业务逻辑单元,当聚合之间需要交互时,为了避免在领域层聚合之间发生耦合,我们将聚合之间的服务调用上升到应用层,通过应用层的应用服务来组织和协调各个聚合的领域服务,解耦聚合并实现跨聚合的服务调用。
上面是DDD聚合设计时的一些通用原则,还是那句话:"适合自己的才是最好的。"在微服务设计时,需要根据项目和团队的具体情况来考虑是否遵循这些原则,一切以优先解决实际问题为出发点。
聚合的设计模式
在聚合设计时,如果聚合内领域对象比较多,领域对象的初始化和持久化就会变得比较复杂。那么你就可以用到这两种非常重要的设计模式:仓储模式(Repository Mode)和工厂模式(Factory Mode)。仓储模式主要完成领域对象持久化,工厂模式则主要用于聚合领域对象的创建和数据初始化。两者有区别,但是联系紧密。
下面我用一个简单的案例来分别说明仓储模式和工厂模式。
[【案例背景】] 维护企业人员信息,管理人员之间的上下级组织关系。
这里先省略领域建模的过程。我们构建了人员聚合,聚合内有人员聚合根(Person),记录人员基本信息,另外还有人员组织关系实体(Relationship),记录人员的上下级关系。人员组织关系实体Relationship类被聚合根Person类引用。一般一人只有一个上级领导,所以人员聚合根和人员组织关系实体是一对一的关系。另外,人员聚合还有人员类型等若干值对象。
/**
* Person聚合根
*/
@Data
public class Person {
String personId;
String personName;
PersonType personType;
Relationship relationship;
int roleLevel;
Date createTime;
Date lastModifyTime;
PersonStatus status;
public Person create(){
this.createTime = new Date();
this.status = PersonStatus.ENABLE;
return this;
}
public Person enable(){
this.lastModifyTime = new Date();
this.status = PersonStatus.ENABLE;
return this;
}
public Person disable(){
this.lastModifyTime = new Date();
this.status = PersonStatus.DISABLE;
return this;
}
}
/**
*人员组织关系实体Relationship
*/
@Data
public class Relationship {
String id;
String leaderId;
String leaderName;
int leaderLevel;
}
下面分别以Person聚合领域对象数据持久化和创建为例,来分别说明仓储模式和工厂模式。
仓储模式
首先来简单了解仓储模式的基本内容。
- 产生背景
我们在做代码检查时,经常会发现有些人在业务逻辑代码中,写入了很多基础层数据处理逻辑相关的代码,或者在业务逻辑代码中直接修改某些数据实体的情况。这样,基础层的数据处理逻辑就会渗透到领域层的业务逻辑代码中,导致领域层与基础层形成紧耦合关系,领域逻辑难以聚焦于领域模型,领域模型中最核心的领域层会依赖外围的基础层资源,违反了"外层依赖内层"的依赖原则。同时,脱离聚合业务规则控制的数据修改,也容易导致聚合内数据不一致的问题。
随着业务发展,当我们需要技术升级或者更换数据库时,这种业务逻辑和数据逻辑紧耦合的关系将会对上层业务逻辑产生致命影响。
为什么在传统架构下,很多人一听到更换数据库时就会特别紧张?
这是因为一旦更换数据库,就意味着需要将业务逻辑和数据处理逻辑进行重新适配,将两者紧耦合的代码进行剥离或调整。而完成这项工作的成本会非常高,严重的时候可能需要重写核心业务逻辑代码,因此技术升级会变得异常困难,对业务运行会产生非常大的不确定性影响。所以很多企业一直艰难地维持着老的技术体系,不愿主动完成技术升级换代。但在新技术不断涌现和技术升级频率如此之高的当下,这样企业很容易背上沉重的技术债。
仓储模式就是用来隔离业务实现逻辑与基础层资源实现逻辑,降低它们之间的耦合和相互影响而产生的。
- 仓储概念
DDD非常重视领域模型,领域层应该更多关注领域逻辑实现。为了解耦领域逻辑和数据处理逻辑,我们在领域层和基础层之间增加了薄薄的一层,这一层就是仓储。
仓储模式包含仓储接口和仓储实现,仓储接口面向领域层提供基础层数据处理相关的访问接口,仓储实现完成仓储接口对应的数据持久化相关的逻辑处理。一个聚合会有一个仓储,统一由仓储来完成聚合数据的持久化。
领域层业务逻辑面向仓储接口编程,当聚合内的实体数据需要持久化时,只需将领域对象DO对象转换成PO持久化对象,然后传递给仓储接口,通过仓储实现完成DO数据的持久化工作。这样领域层就可以更好地聚焦于聚合的领域逻辑,而不必关心实体数据在基础层到底是如何实现持久化的了。
当需要更换数据库等基础资源时,我们只需要调整仓储实现代码,做好仓储实现的数据持久化处理逻辑与新数据库的适配就可以了。由于领域逻辑只通过仓储接口访问基础层实现逻辑,所以在更换基础资源时,只要仓储接口不变就不会影响到领域层的任何领域逻辑。
所以,仓储模式既实现了领域逻辑和数据处理逻辑的解耦,也解决了领域层对基础层的依赖,实现了依赖倒置,因此就可以相对轻松地应对基础资源的技术升级和变更了。
注意
依赖倒置(Dependence Inversion Principle,DIP)设计是指面向接口编程,而不是面向实现编程。这样可以避免业务逻辑与实现逻辑的耦合,在实现逻辑出现变化时,降低对业务逻辑的影响。
- 实现方式
仓储模式包含仓储接口和仓储实现。
仓储接口的实现逻辑非常简单,只需要在仓储接口类中,定义仓储实现的基本接口和参数就可以了。下面我们一起来看看Person仓储接口代码
import ddd.leave.domain.person.repository.po.PersonPO;
/**
* Person仓储接口
*/
public interface PersonRepository {
void insert(PersonPO personPO);
void update(PersonPO personPO);
PersonPO findById(String personId);
PersonPO findLeaderByPersonId(String personId);
}
仓储实现会根据仓储接口的数据处理逻辑要求,调用DAO完成数据查询或数据持久化,如基于聚合根ID的查询,聚合中新增或修改等领域对象数据的持久化操作。如果数据库需要技术升级,我们只需要调整仓储实现的数据处理逻辑,适配新的数据库就可以了,这种调整不会影响领域逻辑。
Person聚合的仓储实现代码
/**
*Person仓储实现
*/
@Repository
public class PersonRepositoryImpl implements PersonRepository {
@Autowired
PersonDao personDao;
@Override
public void insert(PersonPO personPO) {
personDao.save(personPO);
}
@Override
public void update(PersonPO personPO) {
personDao.save(personPO);
}
@Override
public PersonPO findById(String personId) {
return personDao.findById(personId).orElseThrow(() -> new
RuntimeException("未找到用户"));
}
@Override
public PersonPO findLeaderByPersonId(String personId) {
return personDao.findLeaderByPersonId(personId);
}
}
目前适合Java语言的ORM持久化组件比较丰富,如JPA、Mybatis等。但在实现DDD聚合持久化逻辑时,这些组件总是有点美中不足。
相比较而言,JPA更接近DDD的设计思想。比如,它采用充血模型,有聚合根的设计思想,但美中不足是性能不可控。而Mybatis虽然性能可控,但它却采用了贫血模型。
/**
*Person数据库持久化
*/
@Repository
public interface PersonDao extends JpaRepository<PersonPO, String> {
@Query(value = "select p from PersonPO p where p.relationshipPO.personId=?1")
PersonPO findLeaderByPersonId(String personId);
}
在完成仓储接口和实现逻辑,完成DO到PO对象的转换后,在领域层聚合中的领域服务update(Person person)中,就可以调用仓储接口完成数据持久化操作了。由于领域服务只与仓储接口发生调用关系,数据的持久化逻辑在仓储实现中完成,因此在更换数据库时,只要仓储接口不变,领域服务的逻辑就可以一直保持不变。
/**
*Person聚合的领域服务类
*/
@Service
@Slf4j
public class PersonDomainService {
@Autowired
PersonRepository personRepository;
public void update(Person person) {
personRepository.update(personFactory.createPersonPO(person));
}
}
这样就保持了领域层领域逻辑的稳定,实现了领域层与基础层的解耦和依赖倒置。
工厂模式
聚合中实体和值对象等DO对象的创建和持久化是聚合必不可少的操作,对于实体和值对象比较多和依赖关系复杂的聚合,在DO对象创建时,需要确保聚合根和它依赖的对象实例同时被创建。如果将这项工作全部交由聚合根来实现,聚合根构造函数的逻辑将会非常复杂,聚合根也无法聚焦于自身的领域逻辑。
为了让聚合根专注于领域模型,我们尽量将这些比较通用的,与领域模型业务逻辑无关的工作,从聚合根中剥离,将它们放到工厂中实现,通过工厂封装聚合内复杂对象的创建过程,完成聚合根、实体和值对象等DO对象的创建。这样也可以隐藏聚合内DO对象的创建过程,避免暴露聚合的内部结构。
工厂在进行DO对象初始化和持久化操作时,通常会与仓储一起配合来完成,这是因为DO对象的数据初始化和持久化与PO对象是密不可分的。对于复杂聚合,一般在以下两类场景下,DO和PO对象之间数据转换都可以采用工厂模式完成。
▪DO对象构建和数据初始化时,通过仓储先从数据库中获取PO对象,通过工厂完成DO对象的构建和数据初始化;
▪DO对象持久化时,先通过工厂完成从DO到PO对象数据转换,然后通过仓储完成数据持久化。
当然,并不是所有聚合的对象构造都需要用工厂模式来实现。对于领域对象关系简单的聚合,如果构造过程并不复杂,你仍然可以用聚合根构造函数,完成聚合所有依赖对象的构建和数据初始化。
代码createPerson(PersonPO po)中完成了Person聚合DO对象的构建和初始化,代码createPersonPO(Person person)完成从DO对象向PO持久化对象的转换。
/**
* Person聚合的工厂
*/
@Service
public class PersonFactory {
@Autowired
PersonRepository personRepository;
public PersonPO createPersonPO(Person person) {
PersonPO personPO = new PersonPO();
personPO.setPersonId(person.getPersonId());
personPO.setPersonName(person.getPersonName());
personPO.setRoleLevel(person.getRoleLevel());
personPO.setPersonType(person.getPersonType());
personPO.setCreateTime(person.getCreateTime());
personPO.setLastModifyTime(person.getLastModifyTime());
// 完成Person关联实体relationship从DO到PO的转换
RelationshipPO relationshipPO = relationshipPOFromDO(person);
personPO.setRelationshipPO(relationshipPO);
return personPO;
}
public Person createPerson(PersonPO po) {
Person person = new Person();
person.setPersonId(po.getPersonId());
person.setPersonType(po.getPersonType());
person.setRoleLevel(po.getRoleLevel());
person.setPersonName(po.getPersonName());
person.setStatus(po.getStatus());
person.setCreateTime(po.getCreateTime());
person.setLastModifyTime(po.getLastModifyTime());
// 完成Person关联实体relationship从PO到DO的转换
person.setRelationship(relationshipFromPO(po.getRelationshipPO()));
return person;
}
private RelationshipPO relationshipPOFromDO(Person person) {
RelationshipPO relationshipPO = new RelationshipPO();
relationshipPO.setPersonId(person.getPersonId());
relationshipPO.setLeaderId(person.getRelationship().getLeaderId());
return relationshipPO;
}
private Relationship relationshipFromPO(RelationshipPO relationshipPO) {
Relationship relationship = new Relationship();
relationship.setLeaderId(relationshipPO.getLeaderId());
relationship.setLeaderName(relationshipPO.getLeaderName());
return relationship;
}
}
小结
聚合、聚合根、实体和值对象是领域层聚合内非常重要的领域对象,它们之间具有很强的关联性。聚合里包含聚合根、实体、值对象和领域服务等,它们共同按照聚合的业务规则完成聚合的核心领域逻辑。
我们一起来总结一下聚合、聚合根、实体和值对象,看看它们的联系和区别。
聚合的特点:聚合内部业务逻辑高内聚,聚合之间满足低耦合的特点。聚合是领域模型中最小的业务逻辑边界。
在对于有性能扩展或者版本发布频率有极致要求的业务场景中,你也可以将聚合独立拆分为一个微服务,以满足软件版本高频发布和极致弹性伸缩能力的要求。但要记住:聚合虽然可以作为领域模型中拆分为微服务的最小单位,但不要对微服务过度拆分,这样会增加运维和集成成本。虽然在当前微服务架构下,不建议你对微服务过度拆分,但在未来的Serverless架构下,业务功能单元将会变得越来越小,这时聚合这个最小的业务单元可能会演变为最小的部署单元,这时高内聚低耦合的聚合边界将会发挥出巨大的价值。
一个微服务可以有多个聚合,聚合之间的边界是微服务内天然的逻辑边界,它是可拆分的最小积木块。有了这个逻辑边界,在微服务架构演进时,我们就可以以聚合为单位进行拆分和组合了,因此微服务的架构演进也就不再是一件难事了。
聚合根的特点:聚合根是实体,有实体的特点,拥有全局唯一标识,有独立的生命周期。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象通过对象引用的方式进行组织和协调,聚合与聚合之间只能通过聚合根ID引用的方式,实现聚合之间的访问和协同。
实体的特点:实体有ID标识,通过ID判断相等性,ID在聚合内唯一即可。实体的状态可变,它依附于聚合根,其生命周期由聚合根管理。实体一般会持久化,但与持久化对象不一定是一对一的关系。实体可以引用聚合内的聚合根、实体和值对象。
值对象的特点:值对象无ID,且数据不可变,它没有生命周期,用完即扔。值对象通过属性值判断相等性。它是一组概念完整的属性组成的集合,用于描述实体的状态和特征,其核心本质是"值"。