# 聚合、实体、值对象
在领域驱动设计(Domain-Driven Design, DDD)中,领域模型(Domain Model)是核心概念之一。领域模型是对现实世界中业务领域的抽象,它包含了业务领域的聚合(Aggregates)、实体(Entities)、值对象(Value Objects)等概念。
# 聚合对象
聚合是一组相关对象的集合,它们一起形成一个单一的单元。
# 概念
聚合是领域模型中的一个关键概念,它是一组具有内聚性的相关对象的集合,这些对象一起工作以执行某些业务规则或操作。聚合定义了一组对象的边界,这些对象可以被视为一个单一的单元进行处理。
关键:聚合内实现事务一致性、聚合外实现最终一致性。
# 特性
- 一致性边界:聚合确保其内部对象的状态变化是一致的。当对聚合内的对象进行操作时,这些操作必须保持聚合内所有对象的一致性。
- 根实体:每个聚合都有一个根实体(Aggregate Root),它是聚合的入口点。根实体拥有一个全局唯一的标识符,其他对象通过根实体与聚合交互。
- 事务边界:聚合也定义了事务的边界。在聚合内部,所有的变更操作应该是原子的,即它们要么全部成功,要么全部失败,以此来保证数据的一致性。
# 用途
- 封装业务逻辑:聚合通过将相关的对象和操作封装在一起,提供了一个清晰的业务逻辑模型,有助于业务规则的实施和维护。
- 保证一致性:聚合确保内部状态的一致性,通过定义清晰的边界和规则,聚合可以在内部强制执行业务规则,从而保证数据的一致性。
- 简化复杂性:聚合通过组织相关的对象,简化了领域模型的复杂性。这有助于开发者更好地理解和扩展系统。
# 实现手段
- 定义聚合根:选择合适的聚合根是实现聚合的第一步。聚合根应该是能够代表整个聚合的实体,并且拥有唯一标识。
- 限制访问路径:只能通过聚合根来修改聚合内的对象,不允许直接修改聚合内部对象的状态,以此来维护边界和一致性。
- 设计事务策略:在聚合内部实现事务一致性,确保操作要么全部完成,要么全部回滚。对于聚合之间的交互,可以采用领域事件或其他机制来实现最终一致性。
- 封装业务规则:在聚合内部实现业务规则和逻辑,确保所有的业务操作都遵循这些规则。
- 持久化:聚合根通常与数据持久化层交互,以保存聚合的状态。这通常涉及到对象-关系映射(ORM)或其他数据映射技术。
通过这些实现手段,DDD中的聚合模型能够帮助开发者构建出既符合业务需求又具有良好架构设计的软件系统。
以下是一个简化的Java代码示例,展示了如何在DDD中实现一个聚合。在这个例子中,我们将创建一个简单的订单系统,其中包含订单聚合(Order Aggregate)和订单项(OrderItem)作为内部实体。订单聚合根(Order)将封装所有业务规则,并通过聚合根进行所有的交互。
首先,我们定义实体和值对象的基类:
//实体基类
public abstract class BaseEntity{
protected Long id;
public Long getId(){
return id;
}
}
//值对象基类
public abstract class ValueObject{
//值对象通常是不可变的,所以没有setter方法
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
然后,我们定义订单项(OrderItem)作为实体:
public class OrderItem extends BaseEntity{
private String productName;
private int quantity;
private double price;
public Order Item(StringproductName,intquantity,doubleprice){
this.productName = productName;
this.quantity = quantity;
this.price = price;
}
public double getTotalPrice(){
return quantity * price;
}
//省略getter和setter方法
}
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
接下来,我们定义订单聚合根(Order):
importjava.util.ArrayList;
importjava.util.List;
public class Order Aggregate extends BaseEntity{
private List<OrderItem> orderItems;
private String customerName;
private boolean isPaid;
public Order Aggregate(String customerName){
this.customerName = customerName;
this.orderItems = new ArrayList<>();
this.isPaid = false;
}
public void addItem(OrderItem item){
//业务规则:订单未支付时才能添加订单项
if(!isPaid){
orderItems.add(item);
}else{
throw new IllegalStateException("Can not add items to apaid order.");
}
}
public double getTotal Amount(){
return orderItems.stream().mapToDouble(OrderItem::getTotalPrice).sum();
}
public void markAsPaid(){
//业务规则:订单总金额必须大于0才能标记为已支付
if(getTotalAmount() > 0){
isPaid = true;
}else{
throw new IllegalStateException("Order total must begreater than 0 to bepaid.");
}
}
//省略getter和setter方法
}
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
最后,我们可以创建一个订单,并添加一些订单项:
public class Order Demo{
public static void main(String[] args){
//创建订单聚合
OrderAggregate orderAggregate= new OrderAggregate("XiaoFuGe");
//添加订单项
orderAggregate.addItem(new OrderItem("手机",1,1000.00));
orderAggregate.addItem(new OrderItem("数据线",2,25.00));
//获取订单总金额
System.out.println("Total amount:" + orderAggregate.getTotalAmount());
//标记订单为已支付
order Aggregate.markAsPaid();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在这个例子中,我们展示了如何在DDD中定义聚合根和实体,并且如何封装业务规则。订单聚合根(Order)确保了订单项(OrderItem)的一致性,并且只有通过聚合根才能修改订单的状态。这个例子还展示了如何在聚合内部实现事务一致性,例如,订单项只能在订单未支付时添加,订单必须有一个大于0的总金额才能标记为已支付。
# 实体
实体(Entity)在领域驱动设计(Domain-Driven Design, DDD)中是一个核心概念,用于表示具有唯一标识的领域对象。以下是实体的详细介绍:
# 概念
实体 = 唯一标识 + 状态属性 + 行为动作(功能),是DDD中的一个基本构建块,它代表了具有唯一标识的领域对象。实体不仅仅包含数据(状态属性),还包含了相关的行为(功能),并且它的标识在整个生命周期中保持不变。
# 特性
- 唯一标识:实体具有一个可以区分其他实体的标识符。这个标识符可以是一个ID、一个复合键或者是一个自然键,关键是它能够唯一地标识实体实例。
- 领域标识:实体的标识通常来源于业务领域,例如用户ID、订单ID等。这些标识符在业务上有特定的含义,并且在系统中是唯一的。
- 委派标识:在某些情况下,实体的标识可能是由ORM(对象关系映射)框架自动生成的,如数据库中的自增主键。这种标识符虽然可以唯一标识实体,但它并不直接来源于业务领域。
# 用途
- 表达业务概念:实体用于在软件中表达具体的业务概念,如用户、订单、交易等。通过实体的属性和行为,可以描述这些业务对象的特征和能力。
- 封装业务逻辑:实体不仅仅承载数据,还封装了业务规则和逻辑。这些逻辑包括验证数据的有效性、执行业务规则、计算属性值等。这样做的目的是保证业务逻辑的集中和一致性。
- 保持数据一致性:实体负责维护自身的状态和数据一致性。它确保自己的属性和关联关系在任何时候都是正确和完整的,从而避免数据的不一致性。
# 实现手段
在实现实体时,通常会采用以下手段:
- 定义实体类:在代码中定义一个类,该类包含实体的属性、构造函数、方法等。
- 实现唯一标识:为实体类提供一个唯一标识的属性,如ID,并确保在实体的生命周期中这个标识保持不变。
- 封装行为:在实体类中实现业务逻辑的方法,这些方法可以操作实体的状态,并执行相关的业务规则。
- 使用ORM框架:利用ORM框架将实体映射到数据库表中,这样可以简化数据持久化的操作。
- 实现领域服务:对于跨实体或跨聚合的操作,可以实现领域服务来处理这些操作,而不是在实体中直接实现。
- 使用领域事件:当实体的状态发生变化时,可以发布领域事件,这样可以通知其他部分的系统进行相应的处理。
通过上述手段,实体在DDD架构中扮演着重要的角色,它不仅代表了业务概念,还封装了业务逻辑,并通过其唯一标识确保了数据的一致性。
以下是一个简单的Java代码示例,展示了如何在领域驱动设计(DDD)中实现一个实体。我们将创建一个User实体,它代表了一个用户,具有唯一的用户ID、姓名和电子邮件地址,并且可以执行一些基本的行为。
import java.util.Objects;
import java.util.UUID;
//UserEntity实体类
public class UserEntity{
//实体的唯一标识符
private final UUIDid;
//用户的状态属性
private String name;
private String email;
//构造函数,用于创建实体实例
public User Entity(UUID id,String name,String email){
this.id=id;
this.name=name;
this.email=email;
//可以在这里添加验证逻辑,确保创建的实体是有效的
}
//实体的行为方法,例如更新用户的姓名
public void updateName(String newName){
//可以在这里添加业务规则,例如验证姓名的格式
this.name = newName;
}
//实体的行为方法,例如更新用户的电子邮件地址
public void updateEmail(String newEmail){
//可以在这里添加业务规则,例如验证电子邮件地址的格式
this.email=newEmail;
}
//Getter方法
public UUID getId(){
returnid;
}
public String getName(){
return name;
}
public String getEmail(){
return email;
}
//实体的equals和hashCode方法,基于唯一标识符实现
@Override
public boolean equals(Object o){
if(this==o)return true;
if(o==null||getClass()!=o.getClass())return false;
UserEntity user = (UserEntity)o;
return id.equals(user.id);
}
@Override
public int hashCode(){
return Objects.hash(id);
}
//toString方法,用于打印实体信息
@Override
public String toString(){
return"UserEntity{"+
"id="+id+
",name='"+name+'\''+
",email='"+email+'\''+
'}';
}
}
//使用实体的示例
public class UserEntityDemo{
public static void main(String[]args){
//创建一个新的用户实体
UserEntity user=new UserEntity(UUID.randomUUID(),"XiaoFuGe","xiaofuge@qq.com");
//打印用户信息
System.out.println(user);
//更新用户的姓名
user.updateName("XiaoFuGe");
//打印更新后的用户信息
System.out.println(user);
//更新用户的电子邮件地址
user.updateEmail("xiaofuge@qq.com");
//打印更新后的用户信息
System.out.println(user);
}
}
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
在这个例子中,User类代表了用户实体,它有一个唯一的标识符id,这个标识符在实体的整个生命周期中保持不变。name和email是用户的状态属性,updateName和updateEmail是封装了业务逻辑的行为方法。equals和hashCode方法基于唯一标识符实现,以确保实体的正确比较和散列。UserDemo类演示了如何创建和使用User实体。
# 值对象
在领域驱动设计(Domain-Driven Design, DDD)中,值对象(Value Object)是一个核心概念,用于封装和表示领域中的概念,其特点是它们描述了领域中的某些属性或度量,但不具有唯一标识。
值对象 = 值 + 对象,用于描述对象属性的值,表示具体固定不变的属性值信息。
# 概念
值对象是由一组属性组成的,它们共同描述了一个领域概念。与实体(Entity)不同,值对象不需要有一个唯一的标识符来区分它们。值对象通常是不可变的,这意味着一旦创建,它们的状态就不应该改变。
# 特性
- 不可变性(Immutability):值对象一旦被创建,它的状态就不应该发生变化。这有助于保证领域模型的一致性和线程安全性。
- 等价性(Equality):值对象的等价性不是基于身份或引用,而是基于对象的属性值。如果两个值对象的所有属性值都相等,那么这两个对象就被认为是等价的。
- 替换性(Replaceability):由于值对象是不可变的,任何需要改变值对象的操作都会导致创建一个新的值对象实例,而不是修改现有的实例。
- 侧重于描述事物的状态:值对象通常用来描述事物的状态,而不是事物的唯一身份。
- 可复用性(Reusability):值对象可以在不同的领域实体或其他值对象中重复使用。
# 用途
值对象的用途非常广泛,它们可以用来表示:
- 金额和货币(如价格、工资、费用等)
- 度量和数据(如重量、长度、体积等)
- 范围或区间(如日期范围、温度区间等)
- 复杂的数学模型(如坐标、向量等)
- 任何其他需要封装的属性集合
# 实现手段
在实现值对象时,通常会遵循以下几个步骤:
- 定义不可变类:确保类的所有属性都是私有的,并且只能通过构造函数来设置。
- 重写equals和hashCode方法:这样可以确保值对象的等价性是基于它们的属性值,而不是对象的引用。
- 提供只读访问器:只提供获取属性值的方法,不提供修改属性值的方法。
- 使用工厂方法或构造函数创建实例:这有助于确保值对象的有效性和一致性。
- 考虑序列化支持:如果值对象需要在网络上传输或存储到数据库中,需要提供序列化和反序列化的支持。
# 示例
以订单状态为例,可以定义一个值对象来表示不同的状态:
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);
}
}
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
49
50
51
52
53
在这个例子中,OrderStatusVO是一个枚举类型的值对象,它封装了订单状态的代码和描述。它是不可变的,并且提供了基于属性值的等价性。通过定义一个枚举,我们可以确保订单状态的值是受限的,并且每个状态都有一个明确的含义。
在数据库中,订单状态可能会以整数形式存储(例如,0表示下单,1表示支付等)。在应用程序中,我们可以使用OrderStatusVO枚举来确保我们在代码中使用的是类型安全的值,而不是裸露的整数。这样可以减少错误,并提高代码的可读性和可维护性。
当需要将订单状态存储到数据库中时,我们可以存储枚举的code值。当从数据库中读取订单状态时,我们可以使用fromCode方法来将整数值转换回OrderStatusVO枚举,这样我们就可以在代码中使用丰富的枚举类型而不是简单的整数。
值对象也可以用来表示更复杂的结构,比如一个地址:
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);
}
}
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
在这个例子中,AddressVO是一个不可变的值对象,它封装了一个地址的所有部分。它提供了只读访问器,并且重写了equals和hashCode方法以确保基于属性值的等价性。这样的设计有助于确保地址的一致性,并且可以在不同的实体之间重复使用,例如用户和商店都可能有地址。
总的来说,值对象是DDD中用于封装领域概念的重要工具,它们通过提供不可变性、基于属性的等价性和替换性来帮助构建一个清晰、一致和可维护的领域模型。
