DDD架构之领域,聚合,实体,值对象

Domain(领域)

在DDD中,领域是指具体业务领域的知识、业务逻辑、数据以及业务规则的集合。它是软件要解决问题的业务环境,通常由一系列子领域组成,每个子领域代表业务中的一个特定部分。

领域的特性

  1. 业务中心:领域是围绕业务需求和业务规则构建的,它是软件设计的核心。
  2. 模型驱动:领域模型是对业务知识的抽象,它通过领域实体、值对象、服务、聚合等概念来表达。
  3. 语言一致性:领域模型的构建基于统一语言(Ubiquitous Language),这是开发团队与业务专家共同使用的语言,确保沟通无歧义。
  4. 边界清晰:领域模型定义了清晰的边界,这些边界划分了不同的子领域和聚合,有助于管理复杂性和维护性。

领域的用途

业务逻辑的封装:领域模型封装了业务逻辑,使得业务规则和数据操作集中管理,便于理解和维护。
沟通工具:领域模型作为开发团队与业务专家之间的共同语言,有助于提高沟通效率,确保软件开发紧密跟随业务需求。
软件设计的基础:领域模型是软件设计的基础,它指导着软件的架构和实现。

实现手段

  1. 实体(Entity):具有唯一标识的领域对象,代表业务中的实体。
  2. 值对象(Value Object):描述领域中的一些特性或概念,没有唯一标识,通常是不可变的。
  3. 聚合(Aggregate):一组相关的实体和值对象的集合,它们一起构成一个数据和业务规则的单元。
  4. 领域服务(Domain Service):在领域模型中执行特定业务逻辑的无状态服务,通常操作多个实体或聚合。
  5. 领域事件(Domain Event):表示领域中发生的重要业务事件,用于解耦系统的不同部分。
  6. 仓储(Repository):提供对聚合根的持久化操作,如保存和检索,通常与数据库交互。
  7. 领域适配器(Domain Adapter):领域适配器是适配器模式在DDD中的应用,它的目的是使得领域模型能够与外部系统或技术细节进行交互,而不会受到污染。
  8. 工厂(Factory):用于创建复杂的聚合或实体,封装创建逻辑。如 OpenAi项目、Lottery 项目都运用了工厂,也包括如 chatglm-sdk-java 的开发,就是会话模型结构用工厂对外提供服务。

通过这些实现手段,DDD使得软件设计更加贴近业务需求,提高了软件的质量和可维护性。开发团队可以更好地理解业务逻辑,从而设计出更加健壮和灵活的系统。

聚合对象

聚合是一组相关对象的集合,它们一起形成一个单一的单元。

概念

聚合是领域模型中的一个关键概念,它是一组具有内聚性的相关对象的集合,这些对象一起工作以执行某些业务规则或操作。聚合定义了一组对象的边界,这些对象可以被视为一个单一的单元进行处理。

关键:聚合内实现事务一致性、聚合外实现最终一致性。

特性

  1. 一致性边界:聚合确保其内部对象的状态变化是一致的。当对聚合内的对象进行操作时,这些操作必须保持聚合内所有对象的一致性。
  2. 根实体:每个聚合都有一个根实体(Aggregate Root),它是聚合的入口点。根实体拥有一个全局唯一的标识符,其他对象通过根实体与聚合交互。
  3. 事务边界:聚合也定义了事务的边界。在聚合内部,所有的变更操作应该是原子的,即它们要么全部成功,要么全部失败,以此来保证数据的一致性。

用途

  1. 封装业务逻辑:聚合通过将相关的对象和操作封装在一起,提供了一个清晰的业务逻辑模型,有助于业务规则的实施和维护。
  2. 保证一致性:聚合确保内部状态的一致性,通过定义清晰的边界和规则,聚合可以在内部强制执行业务规则,从而保证数据的一致性。
  3. 简化复杂性:聚合通过组织相关的对象,简化了领域模型的复杂性。这有助于开发者更好地理解和扩展系统。

实现手段

  1. 定义聚合根:选择合适的聚合根是实现聚合的第一步。聚合根应该是能够代表整个聚合的实体,并且拥有唯一标识。
  2. 限制访问路径:只能通过聚合根来修改聚合内的对象,不允许直接修改聚合内部对象的状态,以此来维护边界和一致性。
  3. 设计事务策略:在聚合内部实现事务一致性,确保操作要么全部完成,要么全部回滚。对于聚合之间的交互,可以采用领域事件或其他机制来实现最终一致性。
  4. 封装业务规则:在聚合内部实现业务规则和逻辑,确保所有的业务操作都遵循这些规则。
    持久化:聚合根通常与数据持久化层交互,以保存聚合的状态。这通常涉及到对象-关系映射(ORM)或其他数据映射技术。

通过这些实现手段,DDD中的聚合模型能够帮助开发者构建出既符合业务需求又具有良好架构设计的软件系统。

实体

实体(Entity)在领域驱动设计(Domain-Driven Design, DDD)中是一个核心概念,用于表示具有唯一标识的领域对象。以下是实体的详细介绍:

概念

实体 = 唯一标识 + 状态属性 + 行为动作(功能),是DDD中的一个基本构建块,它代表了具有唯一标识的领域对象。实体不仅仅包含数据(状态属性),还包含了相关的行为(功能),并且它的标识在整个生命周期中保持不变。

特性

  1. 唯一标识:实体具有一个可以区分其他实体的标识符。这个标识符可以是一个ID、一个复合键或者是一个自然键,关键是它能够唯一地标识实体实例。
  2. 领域标识:实体的标识通常来源于业务领域,例如用户ID、订单ID等。这些标识符在业务上有特定的含义,并且在系统中是唯一的。
  3. 委派标识:在某些情况下,实体的标识可能是由ORM(对象关系映射)框架自动生成的,如数据库中的自增主键。这种标识符虽然可以唯一标识实体,但它并不直接来源于业务领域。

用途

  1. 表达业务概念:实体用于在软件中表达具体的业务概念,如用户、订单、交易等。通过实体的属性和行为,可以描述这些业务对象的特征和能力。
  2. 封装业务逻辑:实体不仅仅承载数据,还封装了业务规则和逻辑。这些逻辑包括验证数据的有效性、执行业务规则、计算属性值等。这样做的目的是保证业务逻辑的集中和一致性。
  3. 保持数据一致性:实体负责维护自身的状态和数据一致性。它确保自己的属性和关联关系在任何时候都是正确和完整的,从而避免数据的不一致性。

实现手段

在实现实体时,通常会采用以下手段:

  1. 定义实体类:在代码中定义一个类,该类包含实体的属性、构造函数、方法等。
  2. 实现唯一标识:为实体类提供一个唯一标识的属性,如ID,并确保在实体的生命周期中这个标识保持不变。
  3. 封装行为:在实体类中实现业务逻辑的方法,这些方法可以操作实体的状态,并执行相关的业务规则。
  4. 使用ORM框架:利用ORM框架将实体映射到数据库表中,这样可以简化数据持久化的操作。
  5. 实现领域服务:对于跨实体或跨聚合的操作,可以实现领域服务来处理这些操作,而不是在实体中直接实现。
  6. 使用领域事件:当实体的状态发生变化时,可以发布领域事件,这样可以通知其他部分的系统进行相应的处理。

通过上述手段,实体在DDD架构中扮演着重要的角色,它不仅代表了业务概念,还封装了业务逻辑,并通过其唯一标识确保了数据的一致性。

值对象

在领域驱动设计(Domain-Driven Design, DDD)中,值对象(Value Object)是一个核心概念,用于封装和表示领域中的概念,其特点是它们描述了领域中的某些属性或度量,但不具有唯一标识。

值对象 = 值 + 对象,用于描述对象属性的值,表示具体固定不变的属性值信息。

概念

值对象是由一组属性组成的,它们共同描述了一个领域概念。与实体(Entity)不同,值对象不需要有一个唯一的标识符来区分它们。值对象通常是不可变的,这意味着一旦创建,它们的状态就不应该改变。

特性

  1. 不可变性(Immutability):值对象一旦被创建,它的状态就不应该发生变化。这有助于保证领域模型的一致性和线程安全性。
  2. 等价性(Equality):值对象的等价性不是基于身份或引用,而是基于对象的属性值。如果两个值对象的所有属性值都相等,那么这两个对象就被认为是等价的。
  3. 替换性(Replaceability):由于值对象是不可变的,任何需要改变值对象的操作都会导致创建一个新的值对象实例,而不是修改现有的实例。
  4. 侧重于描述事物的状态:值对象通常用来描述事物的状态,而不是事物的唯一身份。
  5. 可复用性(Reusability):值对象可以在不同的领域实体或其他值对象中重复使用。

用途

值对象的用途非常广泛,它们可以用来表示:

金额和货币(如价格、工资、费用等)
度量和数据(如重量、长度、体积等)
范围或区间(如日期范围、温度区间等)
复杂的数学模型(如坐标、向量等)
任何其他需要封装的属性集合

实现手段

在实现值对象时,通常会遵循以下几个步骤:

定义不可变类:确保类的所有属性都是私有的,并且只能通过构造函数来设置。
重写equals和hashCode方法:这样可以确保值对象的等价性是基于它们的属性值,而不是对象的引用。
提供只读访问器:只提供获取属性值的方法,不提供修改属性值的方法。
使用工厂方法或构造函数创建实例:这有助于确保值对象的有效性和一致性。
考虑序列化支持:如果值对象需要在网络上传输或存储到数据库中,需要提供序列化和反序列化的支持。

示例

以订单状态为例,可以定义一个值对象来表示不同的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public enum Order StatusVO{
PLACED(0,"下单"),
PAID(1,"支付"),
COMPLETED(2,"完成"),
CANCELLED(3,"退单");

private final int code;
private final String description;

OrderStatusVO(intcode,String description){
this.code = code;
this.description = description;
}

public int getCode(){
return code;
}

public String getDescription(){
return description;
}

//根据code获取对应的OrderStatus
public static OrderStatusVOfromCode(int code){
for(OrderStatusVO status:OrderStatusVO.values()){
if(status.getCode()==code){
return status;
}
}
throw newIllegalArgumentException("Invalid code for OrderStatus:"+code);
}
}

在这个例子中,OrderStatusVO是一个枚举类型的值对象,它封装了订单状态的代码和描述。它是不可变的,并且提供了基于属性值的等价性。通过定义一个枚举,我们可以确保订单状态的值是受限的,并且每个状态都有一个明确的含义。

在数据库中,订单状态可能会以整数形式存储(例如,0表示下单,1表示支付等)。在应用程序中,我们可以使用OrderStatusVO枚举来确保我们在代码中使用的是类型安全的值,而不是裸露的整数。这样可以减少错误,并提高代码的可读性和可维护性。

当需要将订单状态存储到数据库中时,我们可以存储枚举的code值。当从数据库中读取订单状态时,我们可以使用fromCode方法来将整数值转换回OrderStatusVO枚举,这样我们就可以在代码中使用丰富的枚举类型而不是简单的整数。

值对象也可以用来表示更复杂的结构,比如一个地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public final classAddressVO{
private final Stringstreet;
private final Stringcity;
private final StringzipCode;
private final Stringcountry;

publicAddress(Stringstreet,Stringcity,StringzipCode,Stringcountry){
//这里可以添加验证逻辑以确保地址的有效性
this.street=street;
this.city=city;
this.zipCode=zipCode;
this.country=country;
}

//只读访问器
public String getStreet(){
returnstreet;
}

public String getCity(){
returncity;
}

public String getZipCode(){
return zipCode;
}

public String getCountry(){
return country;
}

//重写equals和hashCode方法
@Override
public boolean equals(Objecto){
if(this==o)returntrue;
if(o==null||getClass()!=o.getClass())returnfalse;
Address address=(Address)o;
return street.equals(address.street)&&
city.equals(address.city)&&
zipCode.equals(address.zipCode)&&
country.equals(address.country);
}

@Override
public int hashCode(){
return Objects.hash(street,city,zipCode,country);
}
}

在这个例子中,AddressVO是一个不可变的值对象,它封装了一个地址的所有部分。它提供了只读访问器,并且重写了equals和hashCode方法以确保基于属性值的等价性。这样的设计有助于确保地址的一致性,并且可以在不同的实体之间重复使用,例如用户和商店都可能有地址。

总的来说,值对象是DDD中用于封装领域概念的重要工具,它们通过提供不可变性、基于属性的等价性和替换性来帮助构建一个清晰、一致和可维护的领域模型。

References

小傅哥 bugstack 虫洞栈