不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

领域驱动战略篇(5) 实践原则规范

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

未经允许不得转载:菡萏如佳人 » 领域驱动战略篇(5)

欢迎加入极客江湖

进入江湖关于作者