”域”字上,用来确定范围的,fff88f">范围即边界 ,这也是DDD在设计中不...">

核心概念

通用模型
image.png
NAT网关模型
image.png

1. 领域

汉语词典的解释:领域是从事一种专门活动或事业的范围 、部类或部门。
百度百科的解释:领域具体指一种特定的范围或区域。
两个解释有一个共同点——范围,领域的核心重点落在<font color="#ff0000">”域”字上,用来确定范围的,<span style="background:#fff88f">范围即边界 ,这也是DDD在设计中不断强调边界的原因。

  1. DDD按照一定的规则将业务领域进行细分,<font color="#ff0000">分而治之。
  2. 当领域细分到一定的程度后,DDD会将问题范围限定在特定的边界内
  3. 在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。

    [!tip]
    DDD的领域就是这个边界内要解决的业务问题域。举个例子,网络服务域解决的就是各类网络可达性和不可达性问题,计算服务域解决的是计算资源的分配与调度问题;存储服务域解决的就是数据存储和读取的问题。

2. 子域

既然领域是用来限定业务边界和范围的,那么就会有大小之分,领域越大,业务范围就越大,反之则相反。

DDD的研究方法与自然科学的研究方法类似。当人们在自然科学研究中遇到复杂问题时,通常的做法就是将问题一步一步地<span style="background:#fff88f">细分,再针对细分出来的问题域,<span style="background:#fff88f">逐个深入研究 ,探索和建立所有子域的知识体系 。当所有问题子域完成研究时,我们就建立了全部领域的完整知识体系了。

领域可以进一步划分为子领域。我们把划分出来的<span style="background:#fff88f">多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。对于子域来说还可以继续划分为”子子域“,直到划分为,我们熟悉的、能够快速处理的最小问题集合。这个划分到最小问题集合其实就是<span style="background:#fff88f">聚合,聚合之上就是限界上下文,根据限界上下文建立领域模型,然后设计微服务。

[!note]
贯穿软件设计始终的思想:<font color="#ff0000">分而治之

在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身<span style="background:#fff88f">重要性和<span style="background:#fff88f">功能属性划分为三类子域

  • 核心域
  • 通用域
  • 支撑域

2.1.1. 核心域

决定产品和公司<font color="#ff0000">核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。

[!note]
网络域的核心域——VPC、EIP、ELB、VPCEP、NAT、DC、CC

2.1.2. 通用域

没有太多个性化的诉求,同时被多个子域使用的<span style="background:#fff88f">通用功能子域是通用域。

[!note]
十统一:用户认证、权限管理(IAM);标签管理

2.1.3. 支撑域

还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也<span style="background:#fff88f">不包含通用功能的子域,它就是支撑域。简单来说就是从用户功能上不会直观体现,但是却必不可少。

[!note]
网络域的通用域——CETUS、 NGP

3. 限界上下文

简单理解就是领域所处的<span style="background:#fff88f">环境以及邻域处理问题的边界。理论上,限界上下文的边界就是<span style="background:#fff88f">微服务的边界,因此,限界上下文在DDD设计中一个非常重要的概念。

[!note]
网络域关于微服务拆分的优秀实践,包括CNOS和CETUS两个服务

我们讨论一个问题,通常是在一个限界上下文内去讨论的。对于同一个概念,不同上下文会有不同的理解。

[!note]
如在SNAT规则中,我们通常所说的”后端“是指NAT前的源地址(源主机);而在DNAT规则中,我们通常所说的”后端“是指NAT后的目的地址(目的real server)

4. 实体

  • 特征:拥有<span style="background:#fff88f">唯一标识符,且标识符在历经各种状态变更后仍能保持一致。
  • 对象识别方法:通过唯一标识符识别。
  • 生命周期:对象的<span style="background:#fff88f">延续性和标识会跨越甚至超出软件的生命周期。
  • 可变性:通常是可变的,除了唯一标识符外,其它属性都可以随时间变化。

    [!note]
    在NAT网关这个领域下面,网关实例就是一种实体,其本身具有唯一ID,标识了一个NAT实例,这个NAT实例可以有port(网关口)、session_configuration(自定义会话属性)、TAG、名称、描述等等属性。

与传统数据模型设计优先不同,DDD是针对实际业务场景构建实体对象和行为,<span style="background:#fff88f">先构建领域模型,再将实体对象映射到数据持久化对象。一个实体可以对应0个、1个或者多个PO对象模型。代码的具体实践中,实体又可以具体分为充血模型和贫血模型两种。

4.1. 贫血模型

贫血模型是指<span style="background:#fff88f">数据和业务逻辑分离。领域对象除了对象属性外,只有get和set方法(POJO)。所有的业务逻辑都不包含在内而是放在Service层。
缺点:

  1. 所有逻辑都被抽象到了service层,包括对象最简单的CURD操作,不够OO(面向对象)。
    优点:
  2. 分层清晰,结构清楚
  3. 设计简单,领域模型对象非常稳定
    典型三层设计方法
  • Controller + VO
  • Service + BO
  • Repository + PO

4.2. 充血模型

充血模型是指<span style="background:#fff88f">数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的OO(面向对象)编程风格。
优点:

  1. 符合面向对象设计,对象属性和对象行为绑定
    缺点
  2. 如何划分业务逻辑不够清楚,什么样的逻辑应该放在Domain Object中,什么样的业务逻辑应该放在Service中,这是很含糊的。

    [!note]
    在实践中,我们常用贫血模型,并把对象原生具有的行为、且会被多次重用的行为方法抽象成domain service,以此来和一般的service(usecase)做区分。当然,这个domain service要能够做到不依赖具体基础设施层的实现,不依赖于领域对象外的其他对象模型。在Spring架构中,我们通常可以通过依赖倒置,让domainService依赖repository的接口来实现解耦。

5. 值对象

  1. 特征:无唯一标识,完全由其属性值定义
  2. 对象识别方法:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体
  3. 生命周期:没有独立的生命周期,通常作为实体的一部分

    [!note]
    如NAT网关示例中TCP超时老化时间、UDP超时老化时间、ICMP超时老化时间、非SYN包是否RST、非SYN包是否建链等等一系列相关属性。如果这些属性单独来看,都会显得很碎,如果我们把这一系列属性组合成一个session_configuration集合来看,那这个集合就是值对象。

5.1. 值对象的存储方式

  • 如果值对象属性有被查询过滤的诉求,那么,值对象的每个属性都需要在数据库里单独成为一列;
  • 相反,如果值对象作为一个整体,不会存在单独过滤值对象某个属性的场景,则可以将值对象作为一个大的json直接序列化到数据库中。

6. 聚合

在事件风暴中,我们抽象出我们领域需要的实体值对象 ==> 进而将业务关联紧密的实体和值对象进行组合,形成聚合 ==> 再根据业务语义将多个聚合划定到同一个限定上下文。聚合其实就位于限定上下文和实体之间的一个概念。

聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的。

  • 一个聚合只有一个聚合根,聚合根是可以独立存在的
  • 聚合中其他实体或值对象依赖于聚合根
  • 只有聚合根才能被外部访问到,聚合根维护聚合的内部一致性

聚合有一个<span style="background:#fff88f">聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“<font color="#ff0000">高内聚、低耦合”的。

聚合是位于限界上下文和实体中间的一个概念,一个限界上下文中可能会有多个聚合。一个限界上下文一般对应着一个微服务,随着微服务规模的不断扩大,可能面临进一步拆分的问题,这时候就可以以聚合为单位进行。

[!note]
NAT业务有NAT网关(公网和私网)、NETWORKEP两个聚合,未来可以根据聚合规模的发展情况,将聚合升级为限界上下文,从而完成微服务的进一步拆分。ELB服务有L4和L7两个聚合。

6.1. 聚合原则

  1. 聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,<span style="background:#fff88f">边界之外的任何东西都与该聚合无关,这就是聚合能实现业务<span style="background:#fff88f">高内聚的原因。
  2. 设计小聚合。如果聚合设计得过大,聚合会因为包含过多的实体,导致<span style="background:#fff88f">实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。
  3. 通过唯一标识引用其他聚合。聚合之间是通过<font color="#ff0000">关联外部聚合根ID的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。
  4. 边界之外使用最终一致性。<span style="background:#fff88f">聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦。
  5. 通过应用层实现跨聚合的服务调用。为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。

7. 聚合根

如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为<span style="background:#fff88f">根实体,它不仅是实体,还是聚合的管理者。

  1. 首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
  2. 其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
  3. 最后在聚合之间,它还是聚合对外的接口人,以聚合根ID关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。
    也就是说,聚合之间通过聚合根ID关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

    [!note]
    如果简单直白来说,聚合根一般会以<font color="#ff0000">一级模型的形式出现。在NAT模型领域,NAT网关实例就是NAT业务的聚合根,SNAT和DNAT是聚合内的其他实体,要访问SNAT或DNAT,需要先找到网关实例,再找到SNAT和DNAT

8. 领域事件

领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

8.1. 识别方法

在做用户旅程或者场景分析时,我们要捕捉业务、需求人员或领域专家口中的关键词:

  • 如果发生……,则……
  • 当做完……的时候,请通知……
  • 发生……时,则……
    在这些场景中,如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件

    [!note]
    PORT已删除 -> DNAT规则失效
    EIP已冻结 -> SNAT EIP池刷新
    用户已欠费 -> 网关冻结

领域事件驱动设计可以<span style="background:#fff88f">切断领域模型之间的强依赖关系。事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。在领域模型映射到微服务系统架构时,领域事件可以<span style="background:#fff88f">解耦微服务,微服务之间的数据不必要求强一致性,而是基于事件的最终一致性。

8.2. 微服务内

  • 过程调用 -> sk,任务流
  • 事件总线 -> cetus,kafka自发自收

8.3. 微服务间

  • API调用(TCC,try-confirm-concel)
  • 消息中间件
  • 事件总线(Event-Bus)

9. 领域服务

领域中的服务表示一个无状态的操作,它用于实现特定于某个领域的任务。当某个操作不适合放在聚合和值对象上时,最好的方式便是使用领域服务了。可以使用领域服务的情况:

  • 执行一个显著的业务操作
  • 对领域对象进行转换
  • 以多个领域对象作为输入参数进行计算,结果产生一个值对象

10. 应用服务

应用层作为视图层领域层的桥梁,是用来表达用例(usecase)和用户故事的主要手段。

应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只<span style="background:#fff88f">负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。

应用层相对来说是较“薄”的一层,除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,或者向其他系统发生基于事件的消息通知,另外还可以用于创建邮件以发送给客户等。

[!note]
领域服务和应用服务的区别在于:

  1. 领域服务侧重于实现对一个或多个领域对象进行操作、计算的执行动作
  2. 应用服务侧重于编排,本身不应该包含复杂的执行动作