Skip to content

Latest commit

 

History

History
775 lines (613 loc) · 27.2 KB

01_领域层编码指南.md

File metadata and controls

775 lines (613 loc) · 27.2 KB

领域层编码指南

领域层用于实现领域模型,是企业核心业务逻辑的实现方案。

众所周知,在领域模型中,作为聚合根的实体是领域模型中的核心元素。 以聚合根(Aggregate Root)为中心,实体(Enity)、值对象(Value Object)、领域事件(Domain Event)、仓储(Repository)、规约(Specification)、聚合工厂(Domain Model Factory)各司其职,共同组成一个分工协作职责分明的聚合。

而作为业务信息持久化载体的实体和值对象是领域模型中最重要的元素,其结构设计将直接影响聚合内其他元素的设计与实现。

所以,我们可以激进一点地认为,领域模型的设计关键,就是实体(或值对象)及其之间关系的设计。

为了能够在设计实现阶段方便地进行实体模型迭代,cap4j在cap4j-ddd-codegen中提供了两个命令来支持基于db-first模式的实体模型设计。

mvn cap4j-ddd-codegen:gen-entity
mvn cap4j-ddd-codegen:gen-repository

实体持久化映射

这在大部分的场景下,就是对ORM技术的应用。

How to DB First?

简单来说,即通过数据库schema元信息,来完成实体代码生成。 这通过如下映射规则完成:

  • 表 table <--映射--> 实体类 entity class
  • 字段 column <--映射--> 实体属性 entity property
  • 主键字段引用 key reference <--映射--> 实体关系 entity reference

但数据库的shcema元信息只能表达实体类、实体类属性以及实体之间的关系信息。

关于聚合内哪个实体是聚合根?有哪些领域事件?都不确定。

为了增强设计迭代的顺畅性,cap4j设计了一套基于数据库注释(comment)的注解语法,来增强数据库schema的信息表达能力。

这套语法的使用非常简单。 通常情况下(比如都是单实体聚合的领域模型)甚至不需要这些注解语法也可以让ORM代码生成正常工作。

关键注解详细介绍

@R; 支持表注解

指示插件将该表映射成一个聚合根实体。该注解可省略,除非标注@P,否则默认认为是所有表所映射的实体单独构成一个聚合。

@P=parent_entity_table; 支持表注解

指示插件将该表映射成聚合内普通实体,该实体在 parent_entity_table 的实体中有一个引用属性。

@T=JavaType; 支持表注解、列注解

指示插件将该表映射的实体类名或字段类型不按默认规则生成,直接使用注解赋值的 JavaType

@E=整数编号:ENUM_FIELD_NAME:枚举字段注释; 支持列注解

指示插件映射该字段是,该字段需要映射成Java枚举类型,该注解需要配合@T一起工作。需要利用@T注解给枚举类命名。

详细帮助

通过插件help命令可以获得

mvn io.github.netcorepal:cap4j-ddd-codegen-maven-plugin:1.0.0-alpha-2:help
# or
mvn cap4j-ddd-codegen:help

ORM实体生成示例

CREATE TABLE `order` (
       `id` bigint unsigned NOT NULL AUTO_INCREMENT,
       `order_no` varchar(100) NOT NULL DEFAULT '' COMMENT '订单编号',
       `order_status` int unsigned NOT NULL DEFAULT '0' COMMENT '订单状态@T=OrderStatus;@E=0:INIT:待支付|1:PAID:已支付|-1:CLOSED:已关闭;',
       `amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '总金额',
       `version` bigint unsigned NOT NULL DEFAULT '0',
       `db_created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
       `db_updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
       `db_deleted` tinyint(1) NOT NULL DEFAULT '0',
       PRIMARY KEY (`id`),
       KEY `idx_db_created_at` (`db_created_at`),
       KEY `idx_db_updated_at` (`db_updated_at`)
) COMMENT='订单';

CREATE TABLE `order_item` (
      `id` bigint unsigned NOT NULL AUTO_INCREMENT,
      `order_id` bigint NOT NULL DEFAULT '0' COMMENT '关联主订单',
      `name` varchar(100) NOT NULL DEFAULT '' COMMENT '名称',
      `price` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '单价',
      `count` int NOT NULL DEFAULT '0' COMMENT '数量',
      `version` bigint unsigned NOT NULL DEFAULT '0',
      `db_created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      `db_updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
      `db_deleted` tinyint(1) NOT NULL DEFAULT '0',
      PRIMARY KEY (`id`),
      KEY `idx_db_created_at` (`db_created_at`),
      KEY `idx_db_updated_at` (`db_updated_at`)
) COMMENT='订单项\n @P=order';
# 以上sql语句隐含了如下实体映射关系:
# 订单表(order)对应实体是一个聚合根,并且订单项表(order_item)对应实体是order聚合的实体成员。
# 订单表(order)的订单状态字段(order_status)将会映射成OrderStatus的Java类型,该OrderStatus是一个enum类型,有3个字段成员,INIT、PAID、CLOSED

默认情况下,所有数据库表都将会映射成一个Java实体类,该实体类将构成一个聚合,并且作为该聚合的聚合根。如果聚合存在其他实体,则其他实体对应的表注释标注@P注解即可。

@P指示该表对应的Java实体类属于某个聚合内的实体成员。

@E负责生成OrderStatus枚举。@E需要配合@T才能完成数据库字段的Java枚举映射。

@T负责将Order实体的orderStatus字段映射成OrderStatus枚举,@T也可以单独工作,用于DB类型<->Java类型的强制自定义映射。

如果想要对这套语法有个详细完整的了解,可以通过如下maven指令获取语法帮助。 需要注意的是,当前cap4j-ddd-codegen:gen-entity仅支持基于MySQL数据库注释的注解解析,后续会增加其他数据的支持。

核对pom.xml配置的gen-entity所需要的关键信息

<basePackage>项目基础包名,一般为com.yourcompany.project</basePackage>

<connectionString>数据库密码</connectionString>

<user>数据库密码</user>

<pwd>数据库密码</pwd>

<schema>数据库名称</schema>

如果没有问题,先后执行如下命令即可查看代码生成源文件

mvn cap4j-ddd-codegen:gen-entity
mvn cap4j-ddd-codegen:gen-repository

实体(聚合根)

代码位置规范${basePackage}.domain.aggregates.${aggregate}

package org.netcorepal.cap4j.ddd.example.domain.aggregates;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import static org.netcorepal.cap4j.ddd.domain.event.DomainEventSupervisorSupport.events;

import javax.persistence.*;

/**
 * 订单
 *
 * 本文件由[cap4j-ddd-codegen-maven-plugin]生成
 * 警告:请勿手工修改该文件的字段声明,重新生成会覆盖字段声明
 */
@Aggregate(aggregate = "Order", name = "Order", type = "root", description = "订单")
@Entity
@Table(name = "`order`")
@DynamicInsert
@DynamicUpdate
@SQLDelete(sql = "update `order` set `db_deleted` = 1 where `id` = ? and `version` = ? ")
@Where(clause = "`db_deleted` = 0")

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class Order {

    // 【行为方法开始】



    // 【行为方法结束】



    // 【字段映射开始】本段落由[cap4j-ddd-codegen-maven-plugin]维护,请不要手工改动

    @Id
    @GeneratedValue(generator = "org.netcorepal.cap4j.ddd.domain.distributed.SnowflakeIdentifierGenerator")
    @GenericGenerator(name = "org.netcorepal.cap4j.ddd.domain.distributed.SnowflakeIdentifierGenerator", strategy = "org.netcorepal.cap4j.ddd.domain.distributed.SnowflakeIdentifierGenerator")
    @Column(name = "`id`")
    Long id;


    /**
     * 订单编号
     * varchar(100)
     */
    @Column(name = "`order_no`")
    String orderNo;

    /**
     * 订单状态
     * 0:INIT:待支付;-1:CLOSED:已关闭;1:PAID:已支付
     * int unsigned
     */
    @Convert(converter = org.netcorepal.cap4j.ddd.example.domain.aggregates.enums.OrderStatus.Converter.class)
    @Column(name = "`order_status`")
    org.netcorepal.cap4j.ddd.example.domain.aggregates.enums.OrderStatus orderStatus;

    /**
     * 总金额
     * decimal(14,2)
     */
    @Column(name = "`amount`")
    java.math.BigDecimal amount;

    @OneToMany(cascade = { CascadeType.ALL }, fetch = FetchType.EAGER, orphanRemoval = true) @Fetch(FetchMode.SUBSELECT)
    @JoinColumn(name = "`order_id`", nullable = false)
    private java.util.List<org.netcorepal.cap4j.ddd.example.domain.aggregates.OrderItem> orderItems;

    /**
     * 数据版本(支持乐观锁)
     */
    @Version
    @Column(name = "`version`")
    Integer version;

    // 【字段映射结束】本段落由[cap4j-ddd-codegen-maven-plugin]维护,请不要手工改动
}

实体(聚合的普通实体成员)

代码位置规范${basePackage}.domain.aggregates.${aggregate}

package org.netcorepal.cap4j.ddd.example.domain.aggregates.order;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import static org.netcorepal.cap4j.ddd.domain.event.DomainEventSupervisorSupport.events;

import javax.persistence.*;

/**
 * 订单项
 *
 * 本文件由[cap4j-ddd-codegen-maven-plugin]生成
 * 警告:请勿手工修改该文件的字段声明,重新生成会覆盖字段声明
 */
@Aggregate(aggregate = "Order", name = "OrderItem", type = "entity", relevant = { "Order" }, description = "订单项")
@Entity
@Table(name = "`order_item`")
@DynamicInsert
@DynamicUpdate
@SQLDelete(sql = "update `order_item` set `db_deleted` = 1 where `id` = ? and `version` = ? ")
@Where(clause = "`db_deleted` = 0")

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class OrderItem {

    // 【行为方法开始】



    // 【行为方法结束】



    // 【字段映射开始】本段落由[cap4j-ddd-codegen-maven-plugin]维护,请不要手工改动

    @Id
    @GeneratedValue(generator = "org.netcorepal.cap4j.ddd.domain.distributed.SnowflakeIdentifierGenerator")
    @GenericGenerator(name = "org.netcorepal.cap4j.ddd.domain.distributed.SnowflakeIdentifierGenerator", strategy = "org.netcorepal.cap4j.ddd.domain.distributed.SnowflakeIdentifierGenerator")
    @Column(name = "`id`")
    Long id;


    /**
     * 名称
     * varchar(100)
     */
    @Column(name = "`name`")
    String name;

    /**
     * 单价
     * decimal(14,2)
     */
    @Column(name = "`price`")
    java.math.BigDecimal price;

    /**
     * 数量
     * int
     */
    @Column(name = "`count`")
    Integer count;

    /**
     * 数据版本(支持乐观锁)
     */
    @Version
    @Column(name = "`version`")
    Integer version;

    // 【字段映射结束】本段落由[cap4j-ddd-codegen-maven-plugin]维护,请不要手工改动
}

枚举

代码位置规范${basePackage}.domain.aggregates.${aggregate}.enums

package org.netcorepal.cap4j.ddd.example.domain.aggregates.order.enums;

import lombok.Getter;

import javax.persistence.*;
import java.util.HashMap;
import java.util.Map;

/**
 * 本文件由[cap4j-ddd-codegen-maven-plugin]生成
 * 警告:请勿手工修改该文件,重新生成会覆盖该文件
 */
public enum OrderStatus {

    /**
     * 待支付
     */
    INIT(0, "待支付"),
    /**
     * 已关闭
     */
    CLOSED(-1, "已关闭"),
    /**
     * 已支付
     */
    PAID(1, "已支付"),
    ;
    @Getter
    private int code;
    @Getter
    private String name;

    OrderStatus(Integer code, String name){
        this.code = code;
        this.name = name;
    }

    private static Map<Integer, OrderStatus> enums = null;
    public static OrderStatus valueOf(Integer code) {
        if(enums == null) {
            enums = new HashMap<>();
            for (OrderStatus val : OrderStatus.values()) {
                enums.put(val.code, val);
            }
        }
        if(enums.containsKey(code)){
            return enums.get(code);
        }
        throw new RuntimeException("枚举类型OrderStatus枚举值转换异常,不存在的值" + code);
    }

    /**
     * JPA转换器
     */
    public static class Converter implements AttributeConverter<OrderStatus, Integer>{
        @Override
        public Integer convertToDatabaseColumn(OrderStatus  val) {
            return val.code;
        }

        @Override
        public OrderStatus convertToEntityAttribute(Integer code) {
            return OrderStatus.valueOf(code);
        }
    }
}

聚合仓储

代码位置规范${basePackage}.adapter.domain.repositories

package org.netcorepal.cap4j.ddd.example.adapter.domain.repositories;

import org.netcorepal.cap4j.ddd.example.domain.aggregates.Order;

/**
 * 本文件由[cap4j-ddd-codegen-maven-plugin]生成
 */
public interface OrderRepository extends org.netcorepal.cap4j.ddd.domain.repo.AggregateRepository<Order, Long> {
    // 【自定义代码开始】本段落之外代码由[cap4j-ddd-codegen-maven-plugin]维护,请不要手工改动

    @org.springframework.stereotype.Component
    @org.netcorepal.cap4j.ddd.domain.aggregate.annotation.Aggregate(aggregate = "Order", name = "Order", type = "repository", description = "")
    public static class OrderJpaRepositoryAdapter extends org.netcorepal.cap4j.ddd.domain.repo.AbstractJpaRepository<Order, Long>
    {
        public OrderJpaRepositoryAdapter(org.springframework.data.jpa.repository.JpaSpecificationExecutor<Order> jpaSpecificationExecutor, org.springframework.data.jpa.repository.JpaRepository<Order, Long> jpaRepository) {
            super(jpaSpecificationExecutor, jpaRepository);
        }
    }

    // 【自定义代码结束】本段落之外代码由[cap4j-ddd-codegen-maven-plugin]维护,请不要手工改动
}

领域事件定义与发布

领域事件是发布订阅模式在领域模型中的应用,他将聚合之间的业务依赖关系给独立出来,使得聚合之间没有任何依赖。

基于Outbox模式,cap4j可以实现分离聚合间的持久化变更事务,并能通过内置的补偿机制,自动实现聚合间事务的最终一致性。

当然,这个事务分离的选择是由设计人员控制的,我们可以基于实际场景的需要,选择是否需要开启这一特性。

而在应用这一特性之前,我们需要做一些基本的依赖部署工作。

创建发件箱表

为了实现Outbox模式,本地业务库中需要创建标准发件箱表。

这一步非常简单,利用插件gen-arch命令对项目结构初始化后,项目内resources/ddl.sql即包含了完整的发件箱表建表SQL语句。只需将里面的语句在服务所在业务数据库中执行即可。

这里有个隐含的原则,即我们的服务都有独属于自己的业务数据库。这很重要,如果对这个原则有疑惑,可以基于issue讨论这个原则的必要性。默认大家都认同这个普遍共识的原则。

领域事件定义

代码位置规范${basePackage}.domain.aggregates.${aggregate}.events

cap4j的领域事件发布需配合UnitOfWork模式实现,因为领域事件的发布与聚合内实体属性状态变更的持久化是捆绑的,这也是领域事件一般定义在领域层(domain)的原因。

通过DomainEvent注解的类,会被cap4j识别为领域事件。

DomainEvent 注解属性persist设置成 true,cap4j将会认为该领域事件的传播需要事务隔离,并将应用Outbox模式进行事件的传播。

后续即可通过DomainEventSupervisor接口的attach方法来向当前线程上线文附加领域事件。 一旦 UnitOfWork接口的save()方法成功提交事务。则cap4j将会保障事件被各个订阅方至少消费一次。

@DomainEvent注解属性详解

  • value() 领域事件名称。
  • persist() 事件发布记录持久化控制。可以通过persist=true控制事件进入发件箱表,并脱离事件所在实体持久化变更上下文事务中。以避免订阅逻辑异常影响发布事务的完成。

应用场景例子说明

  • 消费方与订阅方事务隔离 @DomainEvent(persist=true)
  • 消费方与订阅方同一事务 @DomainEvent

关于领域事件与集成事件

集成事件指会对系统内其他服务发布的领域事件。通常如果要区分领域事件和集成事件,那么领域事件一般指的是没有外部服务关注的业务事件,仅在内部聚合之间应用。

辅助代码生成

方式一:数据库注解,在聚合根对应的表注释中使用注解声明领域事件

@DE=OrderPlaced;

方式二:gen-design命令

mvn cap4j-ddd-codegen:gen-design -Ddesign=de:order.Order:OrderPlaced:下单领域事件

代码示例

package org.netcorepal.cap4j.ddd.example.domain.aggregates.events;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.netcorepal.cap4j.ddd.domain.event.annotation.DomainEvent;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 下单领域事件
 *
 * @author bingking338
 */
@DomainEvent(
        persist = true
)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class OrderPlacedDomainEvent {
    /**
     * 订单号
     */
    String orderNo;
    /**
     * 订单金额
     */
    BigDecimal amount;
    /**
     * 下单时间
     */
    LocalDateTime orderTime;
}

领域事件发布

代码位置规范${basePackage}.domain.aggregates.${aggregate}.${AggregateRoot}

重点:领域事件从定义上来说就是基于业务实体状态的变更行为所产生产生的,所以我们应在实体行为中发布领域事件,

领域事件的发布分两个步骤完成

  1. 事件附加到实体: 通过DomainEventSupervisor.java接口的attach方法在实体行为方法内完成。可以通过 import staticRequestSupervisorSupport的静态方法events()引入到实体类中,插件默认配置已支持。
  2. 关联实体持久化事务提交成功: 在应用层的命令中调用UnitOfWork接口的save()方法。

领域事件附加有三种方式

即时发送 DomainEventSupervisorSupport.events().attach(Object eventPayload, Object entity)

延时发送 DomainEventSupervisorSupport.events().attach(Object eventPayload, Object entity, Duration delay)

定时发送 DomainEventSupervisorSupport.events().attach(Object eventPayload, Object entity, LocalDateTime schedule)

小技巧:可以通过静态导入,更方便地调用领域事件管理器。

import static org.netcorepal.cap4j.ddd.domain.event.DomainEventSupervisorSupport.events;

发送重试

以Outbox模式传播的领域事件内部会有重试机制来保障事务的最终一致性,以实现领域事件被各个订阅方至少消费一次的承诺。 但是为了保障系统不会因为传播链路的故障出现无限重试,这个至少一次也是有前提的,通过@Retry我们可以配置事件传播的失效以及重试次数。

代码示例

import static org.netcorepal.cap4j.ddd.domain.event.DomainEventSupervisorSupport.events;


// 代码省略...
public class Order {
    // 代码省略...
    public class Order {

        // 【行为方法开始】

        /**
         * 下单初始化
         * @param items
         */
        public void init(List<OrderItem> items){
            // 代码省略...
            events().attach(OrderPlacedDomainEvent.builder()
                    .orderNo(this.orderNo)
                    .amount(this.amount)
                    .orderTime(LocalDateTime.now())
                    .build(), this);
        }

        // 【行为方法结束】
        // 代码省略...
    }
}

聚合工厂(领域模型工厂)

代码位置规范${basePackage}.domain.aggregates.${aggregate}

领域模型工厂负责聚合根实例的初始创建。

推荐有两种编码风格来实现领域模型工厂。

工厂方法模式

在聚合根中定义静态方法。优势:简单方便

代码示例

// 代码省略...
@Builder
public class Order {
    // 代码省略...
    public class Order {

        // 【行为方法开始】

        /**
         * 工厂方法
         */
        public static Order create(){
            // 代码省略...
            return builder().build();
        }

        // 【行为方法结束】
        // 代码省略...
    }
}

创建聚合工厂服务

优势:模型完整性更强,设计掌控力度更大。

实现 AggregateFactory接口。并标记@Service注入Spring IoC容器。

辅助代码生成

方式一:数据库注解,在聚合根对应的表注释中使用注解声明聚合工厂

@Fac;

方式二:gen-design命令

mvn cap4j-ddd-codegen:gen-design -Ddesign=fac:order.Order:订单工厂

代码示例

package org.netcorepal.cap4j.ddd.example.domain.aggregates.order.factory;

import org.netcorepal.cap4j.ddd.example.domain.aggregates.order.Order;
import lombok.Builder;
import lombok.Data;
import org.netcorepal.cap4j.ddd.domain.aggregate.AggregatePayload;

import java.math.BigDecimal;

/**
 * todo: 工厂负载描述
 *
 * @author cap4j-ddd-codegen
 * @date 2024/09/09
 */
@Data
@Builder
public class OrderPayload implements AggregatePayload<Order> {
    private String orderNo;
    //...
}
package org.netcorepal.cap4j.ddd.example.domain.aggregates.order.factory;

import org.netcorepal.cap4j.ddd.example.domain.aggregates.order.Order;
import org.netcorepal.cap4j.ddd.example.domain.aggregates.order.enums.OrderStatus;
import org.netcorepal.cap4j.ddd.domain.aggregate.AggregateFactory;
import org.springframework.stereotype.Service;

/**
 * TODO: 聚合工厂
 *
 * @author cap4j-ddd-codegen
 * @date 2024/09/09
 */
@Service
public class OrderFactory implements AggregateFactory<OrderPayload, Order> {

    @Override
    public Order create(OrderPayload payload) {

        return Order.builder()
                .orderStatus(OrderStatus.INIT)
                .orderNo(payload.getOrderNo())
                //...
                .build();
    }
}

工作单元 (实现由cap4j组件提供,无需编码)

UnitOfWork模式的具体实现。工作单元通常在应用层(application)使用。 UnitOfWork 其仅有三个系列方法

persist: 附加实体状态变更同步到持久化源意图到持久化上下文,save方法调用后,状态变更将自动同步到持久化源。 remove: 附加实体从持久化源中移除意图到持久化上下文,save方法调用后,实体将自动同步到持久化源。 save: 以上persistremove的意图以事物方式同步到持久化源。

持久化源即数据库,比如Mysql数据库。

仓储(实现由插件自动生成,无需编码)

代码位置规范${basePackage}.adapter.domain.repositories

仓储负责聚合根实例的检索查询需求。仓储严格遵循以集合(Collection)的抽象方式来管理存在持久化源中的聚合实体。

而实体存入持久化源的职责将完全由UnitOfWork承接实现。

基于以上的原则,cap4j实现了完全无需额外开发的聚合仓储存取机制。仓储代码可完全由cap4j-ddd-codegen:gen-repository命令自动生成。

而UnitOfWork的实现由cap4j组件内部实现提供可以直接使用。

以上代码完全依赖成熟的Spring JPA来实现。

仓储接口实现AbstractJpaRepository 中的所有 Object condition 参数仅支持 org.springframework.data.jpa.domain.Specification 实例。

详细使用请参照 org.springframework.data.jpa.repository.JpaSpecificationExecutor 接口。

规约

代码位置规范${basePackage}.domain.aggregates.${aggregate}.specs

实现规约接口Specification 并标记@Service注入Spring IoC容器,当对应聚合根持久化时会自动获取对应实体聚合类型的规约实例,并自动调用specify接口执行校验。

辅助代码生成

方式一:数据库注解,在聚合根对应的表注释中使用注解声明实体规格约束

@Spe;

方式二:gen-design命令

mvn cap4j-ddd-codegen:gen-design -Ddesign=spe:order.Order:订单规约

代码示例

package org.netcorepal.cap4j.ddd.example.domain.aggregates.order.specs;

import org.netcorepal.cap4j.ddd.example.domain.aggregates.order.Order;
import org.netcorepal.cap4j.ddd.domain.aggregate.Specification;
import org.springframework.stereotype.Service;

/**
 * todo: 实体规格约束描述
 *
 * @author cap4j-ddd-codegen
 * @date 2024/09/09
 */
@Service
public class OrderSpecification implements Specification<Order> {
    @Override
    public Result specify(Order entity) {
        return Result.pass();
    }
}

领域服务

代码位置规范${basePackage}.domain.services

@DomainService(name = "领域服务名称")

领域服务需标记@DomainService注解,并标记@Service注入Spring IoC容器。 慎用领域服务,警惕导致领域模型贫血化。

辅助代码生成

方式一:数据库注解,在聚合根对应的表注释中使用注解声明实体规格约束

@Svc;

方式二:gen-design命令

mvn cap4j-ddd-codegen:gen-design -Ddesign=svc:Some:领域服务说明

代码示例

package org.netcorepal.cap4j.ddd.example.domain.services;

import org.netcorepal.cap4j.ddd.domain.service.annotation.DomainService;
import org.springframework.stereotype.Service;

/**
 * todo: 领域服务描述
 *
 * @author cap4j-ddd-codegen
 * @date 2024/9/9
 */
@DomainService
@Service
public class SomeDomainService {
}