Dawn's Blogs

分享技术 记录成长

0%

微服务架构设计模式 (5) 微服务架构中的业务逻辑设计

事务脚本模式

对于简单的业务逻辑,可以使用事务脚本模式,事务脚本模式是面向过程而不是面向对象的。事务脚本模式就是,对于每一种系统操作,都有一个方法专门用于实现这个系统操作,使用数据库访问对象(Dao)访问数据库,而数据对象是纯数据几乎没有行为。

事务脚本模式的重要特征就是实现行为的类于存储状态的类是分开的。

image-20230521212736373

领域模型模式

对于复杂的业务,就需要使用领域模型模式了,领域模型模式是面向对象的。领域模型模式将业务逻辑组织为具有状态和行为的类构成的对象模型。这样的设计中,有些类只有状态或者行为,但是很多类同时包含状态和行为。

这样设计的好处是:

  • 易于理解和维护。它不像事务脚本模式由一个完成所有事情的大类组成,而是由许多小类组成,每个小类都有少量的职责。
  • 更容易测试。每一个对象都能够被独立的测试。
  • 更容易扩展。通常而言,面向对象的设计可以使用设计模式,这样提供了易于扩展的可能性。

image-20230521213659409

领域驱动设计

领域模型模式需要用领域驱动设计(DDD)来优化,DDD 是对面向对象设计的改进,是开发复杂业务逻辑的一种方法。在领域驱动设计中,由以下几种基本元素组成:

  • Entity:具有状态的对象。
  • Value Object:不具有状态的对象,仅有值,具有相同的值对象可以互换使用。
  • Factory:复杂实现对象创建的方法或者对象。
  • Repository:用来访问持久化实体的对象,Repository 也封装了访问数据库的底层机制。
  • Service:实现业务逻辑的对象。

领域驱动设计聚合模式

传统的领域模型缺少明确的边界,这会导致一些问题。所以使用领域驱动设计中的聚合模式,聚合具有明确的边界。

聚合是一个边界内领域对象的集群,可以将其视为一个单元。使用聚合有两个好处:

  • 消除了服务之间的对象引用。
  • 确保了本地事务 ACID 在每一个服务内部(对于服务之间的事务性保证,则通过 Saga 实现)。

image-20230522140855911

所有的操作都作用于整个聚合而不是部分聚合。

聚合的规则

领域驱动设计要求聚合遵守一组规则:

  • 只引用聚合根:聚合根(Aggregate Root)是聚合中唯一可以由外部类引用的部分,客户端只能调用聚合根上的方法来更新聚合。
  • 聚合间的引用必须使用主键:聚合间的引用使用主键(如唯一 ID,可以看成是外键),而不是对象引用。它确保聚合之间的边界得到了良好的定义,不会出现跨服务的对象引用问题
  • 一个事务中,只能创建或者更新一个聚合:确保单个事务的范围不超过服务的边界,使用 Saga 可以解决创建或者更新多个聚合。

image-20230522140843639

对于第三点规则,只能说是仁者见仁智者见智,在一个事务中更新多个聚合感觉也是可以的,把在一个服务中的多个聚合更新操作作为同一个本地事务。可以把这几个聚合合并起来,作为一个更大的聚合,聚合的边界是十分主观的事情。

聚合的粒度

一个重要的方面是决定聚合的粒度。通常而言,一个细粒度的聚合可以提高性能,改善用户体验(因为降低了两个用户同时更新同一个聚合的可能性)。一个粗粒度的聚合,因为聚合是事务的范围,则可以更加满足事务的原子性要求。

但是,粗粒度的聚合有以下几点弊端:

  • 降低了可扩展性
  • 因为聚合是事务的范围,事务的范围扩大了,所以可能会产生更多的冲突
  • 更大的聚合称为服务分解的障碍,同一个聚合内的模块需要放在同一个服务上,这使得服务变得更大

基于以上的弊端,我们更加倾向于细粒度的聚合

领域事件

在聚合被创建、或者状态发生重大变更时,会对外发布领域事件

在命名领域事件时,通常采用动词的过去分词,如 OrderCreated 表示订单创建这一领域事件。领域事件通常还具有元数据,如事件 ID 和时间戳(也可能是其他的,只是举个例子)。

在领域事件的接收方,如果接收到了领域事件,在处理领域事件时可能需要更详细的信息。可以从发布领域事件的服务中检索信息。也可以使用事件增强的方法,事件包含接收方需要的信息,因此接收方不需要再额外的查询详细信息。

在发布领域事件时,与发布事务性消息一样,可以引入数据库中间表 OUTBOX,先将数据插入到 OUTBOX 中接着再发布到消息代理中。

领域驱动设计

下面明确一下什么是领域驱动设计(DDD,Domain Driven Design),一种处理高复杂度领域的设计方法,它试图分离技术实现的复杂性,并围绕业务概念构建领域模型来控制业务的复杂性,以解决软件难以理解,难以演进的问题。DDD 包括战略设计和战术设计:

  • 战略设计:建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
  • 战术设计:侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、服务和仓库等代码逻辑的设计和实现。

DDD 就是将复杂的问题划分成许多个小问题(小领域),去分别的解决各自领域内的问题。

概念

在 DDD 中有许多基础概念需要解释。

界限上下文

可以将界限上下文拆分为两个词:界限上下文。限界就是领域的边界,而上下文则是语义环境。通过领域的限界上下文,就可以在统一的领域边界内用统一的语言进行交流

比如商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。

通过领域上下文,可以保证概念在领域内部是统一的、没有二义性的。这个边界定义了模型的适用范围,同样也可能成为微服务设计的参考。

实体和值对象

实体(Entity)具有唯一标识符,是有状态的对象。实体类通常采用充血模型。

值对象(Value Object)没有状态的对象,值对象只能通过属性值区分,如果两个值对象的属性完全相同则视为同一个对象。

聚合和聚合根

领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合。聚合是数据存储和持久化的基本单元,一个聚合对应一个 Repository,从而实现数据持久化。聚合也是可以拆分为微服务的最小单位。一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。有了这个逻辑边界,在微服务架构演进时就可以以聚合为单位进行拆分和组合。

聚合根可以看作是聚合的负责人,也称为根实体,它不仅是实体,还是聚合的管理者。聚合根还是聚合对外暴露的接口,客户端只能调用聚合根上的方法。

聚合的规则前文已经解释了,这里不再赘述。

模型形态

三种模型

  • 充血模型:不仅包含了对象的属性以及访问修改属性的方法(get 和 set),而且还包含了如验证、持久化等的业务逻辑。

  • 失学模型:仅仅包含对象的属性以及访问修改属性的方法(get 和 set)。

  • 贫血模型:类似于充血模型,但是不包含持久化相关的逻辑。

四种对象

  • PO(Persistent Object,持久化对象):直接从数据库读取出来,经过 ORM 映射得到的对象,可以理解为 model。
  • DO(Domain Object,领域对象):从数据库取出来之后,需要从数据还原成实体,这个实体就是 DO。至于为什么不能直接用 PO 作为 DO,这是因为数据库中读取出来的对象与 DO 不是一一对应的,比如一个用户实体的信息需要通过用户表、权限表共同构建出来。
  • DTO(Data Transfer Object,数据传输对象):这种对象存在于 Controller 和 Service 之间。
  • VO(View Object,视图对象):在给客户端返回时,不仅需要 DTO,可能还需要状态码、提示文本等信息,这就引入了 VO。