前情提要:一共包含 如下六篇文章(篇幅精简,快速入门)
1、初识DDD
2、聚合、实体、值对象
3、仓储,封装持久化数据
4、端口和适配器
5、领域事件
6、领域服务,实现约定
DDD设计方法-2-聚合、实体、值对象(概览)
- 1、概览
- 1.1、值对象
- 举例子:
- 1.2、实体对象
- 举例子:
- 1.3、聚合
- 举例子:
- 答疑
- 和之前的vo bo dto 到底有什么差别, 就是名称改了个叫法吗?
- 聚合的实际作用是什么?为什么我要选择用聚合?
1、概览
在软件工程和领域驱动设计(DDD)中,聚合、实体和值对象是三个重要的概念,它们帮助我们构建清晰且有组织的领域模型。以下是对这三个概念的简要概览和示例;
介绍的时候首先带入一个场景: 如下例子基于 户口管理 系统举例
1.1、值对象
首先要知道值对象的特点是什么?然后你才能在程序之中使用;
1、不变;一旦被建立不应该被修改,而应该重新创建
2、没有唯一标识符
3、如何此值对象每一个属性值都相等,那么认为对象相等;
举例子:
地址 是一个值对象,通过其属性(如省、市、区县、街道)进行比较和区分,通常是不可变的。
public final class Address {
//省
private final String province;
//地市
private final String city;
//区县
private final String district;
//乡镇
private final String town;
// 构造函数
public Address(String province, String city, String district, String town) {
this.province = province;
this.city = city;
this.district = district;
this.town = town;
}
}
1.2、实体对象
首先要知道实体对象的特点是什么?然后你才能在程序之中使用;
1、状态和属性可能会发生变化,但是标识符不变。
2、必有唯一标识符
3、通常代表现实世界中的可变对象。
白话文: 实体 = 唯一标识 + 状态属性 + 行为动作(功能)
举例子:
每个人都有唯一标识符(例如身份证号)。
人的属性可能会变化(如姓名、性别、出生日期等)。
public class Person {
private final String id; // 身份证号或其他唯一标识符
private String name;
private String gender;
private LocalDate birthDate;
public Person(String id, String name, String gender, LocalDate birthDate) {
if (id == null || id.isEmpty()) {
throw new IllegalArgumentException("ID cannot be null or empty");
}
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
if (!"男".equals(gender) && !"女".equals(gender)) {
throw new IllegalArgumentException("Gender must be '男' or '女'");
}
if (birthDate == null || birthDate.isAfter(LocalDate.now())) {
throw new IllegalArgumentException("Birth date cannot be null or in the future");
this.id = id;
this.name = name;
this.gender = gender;
this.birthDate = birthDate;
}
}
1.3、聚合
首先要知道聚合的特点是什么?然后你才能在程序之中使用;
1、包含多个实体(Entity)和/或值对象(Value Object)。
2、其中一个实体被称为聚合根(Aggregate Root) ,它是聚合的唯一入口点。
3、通过聚合根来管理聚合的生命周期和一致性。
举例子:
户口本 作为一个聚合,包含了多个人实体和一个地址值对象。户口本负责管理其内部的实体,并保证其内部状态的一致性。
public class Household {
private final String householdId;
private final List<Person> people;
private final Address address;
public Household(String householdId, Address address) {
this.householdId = householdId;
this.address = address;
this.people = new ArrayList<>();
}
}
使用示例:
public class Test{
public static void main(String[] args) {
// 创建地址值对象
Address address = new Address("北京市", "北京市", "海淀区", "中关村大街");
// 创建户口本聚合
Household household = new Household("HH12345", address);
// 创建人实体
Person person1 = new Person("ID1234567890", "张三", "男", LocalDate.of(1985, 1, 1));
Person person2 = new Person("ID0987654321", "李四", "女", LocalDate.of(1990, 5, 15));
// 将人添加到户口本
household.addPerson(person1);
household.addPerson(person2);
// 打印户口本信息
System.out.println(household);
}
}
答疑
到这里是不是已经有人疑惑了?
和之前的vo bo dto 到底有什么差别, 就是名称改了个叫法吗?
DDD设计方法可以说是针对于业务切割和重新建模的方法。
比较大的区别就是建议在对象建立时对对象的合理性进行校验(这个是针对本章片面的一个说法)
比如说 : 针对上述例子: Person 人进行改造在对象创建时检验其合理性
public Person(String id, String name, String gender, LocalDate birthDate) {
if (id == null || id.isEmpty()) {
throw new IllegalArgumentException("ID cannot be null or empty");
}
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
if (!gender.equals("男") && !gender.equals("女")) {
throw new IllegalArgumentException("Gender must be '男' or '女'");
}
if (birthDate == null || birthDate.isAfter(LocalDate.now())) {
throw new IllegalArgumentException("Birth date cannot be null or in the future");
}
this.id = id;
this.name = name;
this.gender = gender;
this.birthDate = birthDate;
}
这样可以在使用的时候保证无需其他额外校验,你拿到这个对象这个对象的值就是合理的。
聚合的实际作用是什么?为什么我要选择用聚合?
针对上边户口本聚合做实际上使用时添加微小功能
1、确保户口本中的人员不能为空,并且必须有一个且只有一个户主。
2、添加和删除成员:可以添加和删除除户主之外的成员。
3、户主转换功能:可以将现有成员转换为新的户主,原户主变为普通成员。
这样你在使用聚合的时候
- 变更入口统一
- 业务规则和校验集中 :
例如,添加成员时检查成员是否已存在、删除成员时检查是否尝试删除户主、更换户主时检查新户主是否为现有成员等。这些校验确保了数据的一致性和完整性。
- 减少耦合,提高内聚
- 确保数据一致性 :
通过统一的入口进行变更,可以更好地确保数据的一致性。例如,在更换户主时,如果不通过聚合根进行,而是直接修改数据,可能会导致数据不一致的问题。而通过聚合根的方法来进行这些操作,可以确保所有相关的业务逻辑和校验都得到执行,从而保证数据的一致性。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class Household {
private final String householdId;
private final Address address;
private final List<Person> members;
private Person head;
public Household(String householdId, Address address, Person head) {
if (householdId == null || householdId.isEmpty()) {
throw new IllegalArgumentException("Household ID cannot be null or empty");
}
if (address == null) {
throw new IllegalArgumentException("Address cannot be null");
}
if (head == null) {
throw new IllegalArgumentException("Head of household cannot be null");
}
this.householdId = householdId;
this.address = address;
this.head = head;
this.members = new ArrayList<>();
}
public void addMember(Person person) {
if (person == null) {
throw new IllegalArgumentException("Person cannot be null");
}
if (members.contains(person) || head.equals(person)) {
throw new IllegalArgumentException("Person is already a member of the household");
}
members.add(person);
}
public void removeMember(Person person) {
if (person == null || head.equals(person)) {
throw new IllegalArgumentException("Cannot remove the head of household or a null person");
}
members.remove(person);
}
public void changeHead(Person newHead) {
if (newHead == null) {
throw new IllegalArgumentException("New head of household cannot be null");
}
if (!members.contains(newHead)) {
throw new IllegalArgumentException("New head of household must be a current member");
}
members.add(head); // 将当前户主降为普通成员
members.remove(newHead); // 从普通成员中移除新户主
this.head = newHead;
}
....
public List<Person> getMembers() {
return Collections.unmodifiableList(members);
}
}
附:改造后的测试方法
import java.time.LocalDate;
public class Test {
public static void main(String[] args) {
// 创建地址值对象
Address address = new Address("北京市", "北京市", "海淀区", "中关村大街");
// 创建人实体
Person head = new Person("ID1234567890", "张三", "男", LocalDate.of(1985, 1, 1));
Person member1 = new Person("ID0987654321", "李四", "女", LocalDate.of(1990, 5, 15));
Person member2 = new Person("ID1112131415", "王五", "男", LocalDate.of(1992, 7, 20));
// 创建户口本聚合
Household household = new Household("HH12345", address, head);
// 添加成员
household.addMember(member1);
household.addMember(member2);
// 打印户口本信息
System.out.println(household);
// 更换户主
household.changeHead(member1);
// 打印更换户主后的户口本信息
System.out.println(household);
// 删除成员
household.removeMember(member2);
// 打印删除成员后的户口本信息
System.out.println(household);
}
}