文章目录
DDD实践原则规范
DDD设计原则
聚合根
- 业务逻辑优先在聚合根边界内完成
- 对聚合根的设计需要提防上帝对象(God Object),也即用一个大而全的领域对象来实现所有的业务功能
例如:不同的限界上下文中都有各自的Product,有些Product是聚合根,有些不是 -
聚合根的实现应该与框架无关,最好是POJO
- 聚合同时也是存储的单元,这样可以保证持久化的简单,特别适合nosql
- 聚合根之间的引用通过ID完成
- 聚合根内部的所有变更都必须通过聚合根完成,聚合根是整个聚合的管理者
- 如果出现一个事务更新了多个聚合根,重新检查聚合根边界是否出了问题;如果确实是业务需要,可以引入消息机制和事件驱动来保证
- 聚合根不应该引用基础设施
- 外界不应该持有聚合根内部数据结构
- 尽量设计小聚合,以后即使要合并聚合也比拆分更简单
- 聚合根里面没有实体,并不意味着数据库就只有一张表,可以设计成多张表。DB设计和领域建模没有关系
聚合的三大基本规则
- 1 只引用聚合根,客户端只能通过聚合根对聚合进行操作
- 2 聚合间引用必须使用主键
- 3 在一个事务中只能创建或更新一个聚合(微服务中表现几乎完美结合saga做事务管理,在使用关系型数据库可以适当打破)
实体与值对象
- 实体对象表示的是具有一定生命周期(聚合根消亡,其他实体也消亡)并且拥有全局唯一标识(ID)的对象
- 值对象是不变的,也就说一个值对象一旦被创建出来了便不能对其进行变更,如果要变更,必须重新创建一个新的值对象整体替换原有的
- 区分实体和值对象的一个很重要的原则便是根据相等性来判断,前者通过ID来区别,后者通过属性
- 将业务概念尽量建模为值对象
资源库
- 资源库(Repository)就是用来持久化聚合根的,只有聚合根才“配得上”拥有Repository,所以一个聚合对应一个资源库
- 有的DDD实践者甚至认为一个纯粹的Repository只应该包含save和byId两个方法(我们不强求,还要结合具体业务,特别是读操作)
- 保证资源库接口不要混入基础设施的实现
- 在领域层,只有领域服务才依赖于资源库
- 要持久化除根实体在外的其他实体或值对象,必须交由聚合对应的资源库来实现(会不会导致资源库非常庞大?是不是要引入CQRS)
> 还是可以单独更新/替换聚合中的其他实体或值对象的,有一个方法saveAggr(),还可以有saveEntity()方法 - 实体的持久化要依赖聚合根
- 值对象的持久化可独立,通过层级对象来实现(自定义一个拥有id的父类)
工厂
- 简单的聚合根创建过程,直接在聚合根中实现Factory方法
- 复杂度的聚合根创建过程,使用独立的Factory类
- 自定义的创建方法含义是业务上的创建,而构造方法是技术语言上的创建
领域服务
- 聚合根中不合适放置的业务逻辑才考虑放到DomainService中
- 应用服务是必须要有的,领域服务不是,它是对非行为方法的一种妥协(通常是一些业务代理,生成ID类)
命令对象
- 外部向领域模型发起的一次命令操作,Controller都要进行封装
- 写操作一般以command作为后缀,传递给应用服务
- 读操作一般以Representation作为后缀,传递给客户端
public OrderId createOrder(CreateOrderCommand command) ;
public void changeProductCount(String id, ChangeProductCountCommand command) ;
public void pay(String id, PayOrderCommand command) ;
public OrderRepresentation toRepresentation(Order order);
业务中读写操作
- 写操作中,我们需要严格地按照“应用服务 -> 聚合根 -> 资源库”的结构进行编码
- 读操作中,领域模型中的对象不能直接返回给客户端
- 以上三种方式,优先推荐基于数据模型的方式,一些类似于遵循JPA规范的框架,采用基于领域模型的方式也可以;CQRS比较庞大,需要结合业务场景慎重考虑
与工具技术结合使用原则
Lombok
会使用到Lombok的对象一般有领域实体对象、Command、Representation、DomainEvent。
Lombok不能随意使用,不然谁都可以任意调用,破坏了“最小权限”和“完整性”原则
请启用lombok的配置文件
- 在项目的根目录中创建lombok.config文件,内容如下:
lombok.anyConstructor.addConstructorProperties=true # 方便Jackson找到对应的构造函数
lombok.addLombokGeneratedAnnotation = true # 将Lombok生成的代码标记为`Generated`,便于JaCoCo等排除这些代码
实体对象只用@Build和@Getter
- @Builder会在没有自定义构造函数的情况下创建全参构造函数,结合先前的lombok.anyConstructor.addConstructorProperties=true,可以满足Jackson的反序列化
- 外加自定义的create()方法来保证构建的完备性,其内部可以使用builder进行创建
- 为了方便测试代码准备测试数据,测试代码可以调用builder直接构建对象而不用create()方法
@Builder
@Getter
public class Product extends BaseAggregate {
private String id;
private String name;
private String description;
private BigDecimal price;
private Instant createdAt;
private int inventory;
private String categoryId;
public static Product create(String name, String description, BigDecimal price, String categoryId) {
Product product = Product.builder()
.id(newUuid())
.name(name)
.description(description)
.price(price)
.createdAt(Instant.now())
.inventory(0)
.categoryId(categoryId)
.build();
product.raiseEvent(new ProductCreatedEvent(product.getId(), name, description, price, product.getCreatedAt()));
return product;
}
}
值对象只能使用@value
- @value相当于是@Data的不可变形式
- 不要去使用@Build,因为值对象的创建无法保证完备性
- 对于Command对象,应该使用JSR-380中的注解来完成验证
@Value
public class CreateProductCommand {
@NotBlank(message = "产品名字不能为空")
private String name;
@NotBlank(message = "产品描述不能为空")
private String description;
@NotNull(message = "产品价格不能为空")
private BigDecimal price;
@NotBlank(message = "产品所属目录不能为空")
private String categoryId;
}
领域事件需要使用@ConstructorProperties和@Getter
- DomainEvent一般是有继承关系的,Lombok创建父类并不优雅,使用@ConstructorProperties好看一点
- @ConstructorProperties()是Spring提供的显示构造函数注解,
@Getter
public class ProductNameUpdatedEvent extends ProductEvent {
private String oldName;
private String newName;
@ConstructorProperties({"productId", "oldName", "newName"})
public ProductNameUpdatedEvent(String productId, String oldName, String newName) {
super(productId);
this.oldName = oldName;
this.newName = newName;
}
}
中间父类ProductEvent为:
@Getter
public abstract class ProductEvent extends DomainEvent {
private String productId;
protected ProductEvent(String productId) {
this.productId = productId;
}
}
最终的父类DomainEvent为:
@Getter
public abstract class DomainEvent {
private String _id = UuidGenerator.newUuid();
private Instant _createdAt = now();
@Override
public String toString() {
return this.getClass().getSimpleName() + "[" + _id + "]";
}
}
这样既保证了调用方不会乱调构造函数,也保证了Jackson能够正确的反序列化
@Data用于基础设施层
- @Data:主要用于基础设施层,在使用一些框架时如有需要可以使用,比如在java项目中使用mapstruct在各个对象之间进行复制时
类名规范
约束与规范
- 如何检查代码是否范围了约束?
1 使用自定义元注解的方式,比如@Aggregate表示一个聚合根,然后用代码检查聚合根是不是有互相引用的情况
2 聚合根和实体用不同的类后缀,通过解析判断该类后缀文件有没有互相引用
推荐:ArchUnit和jQAssistant