DDD 实战1 - 基础代码模型

2021-02-12  本文已影响0人  原水寒

本文首先介绍一下 DDD 代码模型的分层策略,然后介绍一下各层的职责,最后基于 DDD 开发一个订单中心,来实践 DDD.

DDD 分层策略

image.png

在 DDD 中,共有四层(领域层、应用层、用户接口层、基础设施层),其层级实际上是环状架构。如上图所示。根据整洁架构思想,在上述环状架构中,越往内层,代码越稳定,其代码不应该受外界技术实现的变动而变动,所以依赖关系是:外层依赖内层。按照这个依赖原则,DDD 代码模块依赖关系如下:

image.png
  • 领域层(domain):位于最内层,不依赖其他任何层;
  • 应用层(application):仅依赖领域层;
  • 用户接口层(interfaces):依赖应用层和领域层;
  • 基础设施层(infrastructure):依赖应用层和领域层;
  • 启动模块(starter):依赖用户接口层和基础设施层,对整个项目进行启动。
    注意:interfaces 和 infrastructure 位于同一个换上,二者没有依赖关系。

DDD 各层职责

领域模型层 domain

包括实体、值对象、领域工厂、领域服务(处理本聚合内跨实体操作)、资源库接口、自定义异常等

应用服务层 application

跨聚合的服务编排,仅编排聚合根。包括:应用服务等

用户接口层 interfaces

本应用的所有流量入口。包括三部分:

  1. web 入口的实现:包括 controller、DTO 定义、DTO 转化类
  2. 消息监听者(消费者):包括 XxxListener
  3. RPC 接口的实现:比如在使用 Dubbo 时,我们的服务需要开放 Dubbo 服务给第三方,此时需要创建单独的模块包,例如 client 模块,包含 Dubbo 接口和 DTO,在用户接口层中,去做 client 中接口的实现以及 DTO 转化类

基础设施层 infrastructure

本应用的所有流量出口。包括:

  1. 资源库接口的实现
  2. 数据库操作接口、数据库实现(如果使用mybatis,则包含 resource/*.xml)、数据库对象 DO、DO 转化类
  3. 中间件的实现、文件系统实现、缓存实现、消息实现 等
  4. 第三方服务接口的实现

基于 DDD 开发订单中心

需求:基于 DDD 开发一个订单中心,实现下订单、查询订单等功能
代码:https://github.com/zhaojigang/ordercenter

ordercenter 根模块
├── order-application 应用模块
├── order-domain 领域模块
├── order-infrastructure 基础设施模块
├── order-interfaces 用户接口模块
├── order-starter 启动模块
└── pom.xml 根模块

领域层代码模型

image.png

包依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

引入 spring-boot-autoconfigure:2.4.2,在领域工厂中需要用到 Spring 注解

DDD 标识注解 common.ddd.AggregateRoot

/**
 * 标注一个实体是聚合根
 */
@Documented
@Retention(SOURCE)
@Target(TYPE)
public @interface AggregateRoot {
}

自定义异常 common.exception.OrderException

/**
 * 自定义异常
 */
@Data
public class OrderException extends RuntimeException {
    private Integer code;
    private String message;

    public OrderException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

将自定义异常放在领域层,因为 DDD 推荐使用充血模型,在领域实体、值对象或者领域服务中,也会做一些业务逻辑,在业务逻辑中,可以根据需要抛出自定义异常

资源库接口 io.study.order.repository.OrderRepository

/**
 * 订单资源库接口
 */
public interface OrderRepository {
    /**
     * 保存订单
     *
     * @param order 订单
     */
    void add(Order order);

    /**
     * 根据订单ID获取订单
     * @param orderId
     */
    Order orderOfId(OrderId orderId);
}
  1. 资源库接口放置在领域层,实现领域对象自持久化,同时实现依赖反转。
  2. 依赖反转:将依赖关系进行反转,假设 Order 要做自持久化,那么需要拿到资源库的实现 OrderRepositoryImpl 才行,那么 domain 包就要依赖 infrastructure 包,但是这不符合 外层依赖内层 的原则,所以需要进行依赖反转,由 infrastructure 包依赖 domain 包。实现依赖反转的方式就是在被依赖方中添加接口(例如,在 domain 包中添加 OrderRepository 接口),依赖包对接口进行实现(infrastructure 包中对 OrderRepository 进行实现),这样的好处是,domain 可以完全仅关注业务逻辑,不要关心具体技术细节,不用去关心,到底是存储到 mysql,还是 oracle,使用的数据库框架是 mybatis 还是 hibernate,技术细节的实现由 infrastructure 来完成,真正实现了业务逻辑和技术细节的分离
  3. 资源库的命名推荐:对于资源库,推荐面向集合进行设计,即资源库的方法名采用与集合相似的方法名,例如,保存和更新是 add、addAll,删除时 remove、removeAll,查询是 xxxOfccc,例如 orderOfId,ordersOfCondition,复数使用 xxxs 的格式,而不是 xxxList 这样的格式
  4. 一个聚合具有一个资源库:比如订单聚合中,Order 主订单是聚合根,OrderItem 子订单是订单聚合中的一个普通实体,那么在订单聚合中只能存在 OrderRepository,不能存在 OrderItemRepository,OrderItem 的 CRUD 都要通过 OrderRepository 先获得 Order,再从 Order 中获取 List<OrderItem>,再做逻辑。这样的好处,保证了聚合根值整个聚合的入口,对聚合内的其他实体和值对象的方访问,只能通过聚合根,保证了聚合的封装性

领域工厂 io.study.order.factory.OrderFactory

/**
 * 订单工厂
 */
@Component
public class OrderFactory {
    private static OrderRepository orderRepository;

    @Autowired
    public OrderFactory(OrderRepository repository) {
        orderRepository = repository;
    }

    public static Order createOrder() {
        return new Order(orderRepository);
    }
}

工厂的作用:创建聚合。
工厂的好处:

  1. 创建复杂的聚合,简化客户端的使用。例如 Order 的创建需要注入资源库,订单创建后,可以直接发布订单创建事件。
  2. 可读性好(更加符合通用语言),比如 对于创建订单,createOrder 就比 new Order 的语义更加明确
  3. 更好的保证一致性,防止出错,假设创建两个主订单 Order,两个主订单下分别还要创建多个子订单 OrderItem,每个子订单中需要存储主订单的ID,如果由客户端来设置 OrderItem 中的主订单ID,可能会将A主订单的ID设置给B主订单下的子订单,可能出现数据不一致的问题,具体的示例见 《实现领域驱动》P183。

实体唯一标识 io.study.order.domain.OrderId

import lombok.Value;

/**
 * 订单ID
 */
@Value
public class OrderId {
    private Long id;

    public static OrderId of(Long id) {
        return new OrderId(id);
    }

    public void validId(){
        if (id == null || id <= 0) {
            throw new OrderException(400, "id 为空");
        }
    }
}
  1. 推荐使用强类型的对象作为实体的唯一标识,好处有两个:
    a. 用来避免传参混乱,同时提升接口的可读性,例如 xxx(Long orderId, Long goodsId),假设上述接口第一个参数传了 goodsId,第二个传了 orderId,那么编译期是无法发现的,改为 xxx(OrderId orderId, GoodsId, goodsId) 即可避免,同时可读性也较高。
    b. 唯一标识中会有一些其他行为方法,如果唯一标识使用弱类型,那么这些行为方法将会泄露在实体中
  2. 唯一标识类是一个值对象,推荐值对象设置为不可变对象,使用 @lombok.Value 标注值对象,既可标识该对象为值对象,也可以是该类变为不可变类。例如,表示后的 OrderId 没有 setXxx 方法。
  3. 值对象的行为函数都是无副作用函数(即不能影响值对象本身的状态,例如 OrderId 对象被创建后,不能再使用 setXxx 修改其属性值),如果确实有属性需要变动,值对象需要整个换掉(例如,重新创建一个 OrderId 对象)

聚合根 io.study.order.domain.Order

/**
 * 订单聚合根
 */
@Setter
@Getter
@AggregateRoot
public class Order {
    /**
     * 订单 ID
     */
    private OrderId id;
    /**
     * 订单名称
     */
    private String name;
    /**
     * 订单资源库
     */
    private OrderRepository orderRepository;

    protected Order(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    /**
     * 创建订单
     *
     * @param order
     */
    public void saveOrder(Order order) {
        orderRepository.add(order);
    }

    public void setName(String name) {
        if (name == null) {
            throw new OrderException(400, "name 不能为空");
        }
        this.name = name;
    }

    public void setGoodsId(Long goodsId) {
        if (goodsId == null) {
            throw new OrderException(400, "goodsId 不能为空");
        }
        this.goodsId = goodsId;
    }

    public void setBuyQuality(Integer buyQuality) {
        if (buyQuality == null) {
            throw new OrderException(400, "buyQuality 不能为空");
        }
        this.buyQuality = buyQuality;
    }
}
  1. 聚合根是一个特殊的实体,是整个聚合对外的使者,其他聚合与改聚合沟通的方式只能是通过聚合根
  2. 由于使用工厂来创建 Order,那么 Order 的构造器需要设置为 protected,防止外界直接使用进行创建
  3. 实体单个属性的校验需要在 setXxx 中完成自校验
  4. 实体是可变的、具有唯一标识,其唯一标识通常需要设计成强类型
  5. 聚合中的 XxxRepository 可以通过上述的工厂进行注入,也可以使用“双委派”机制,即提供类似方法:createOrder(Order order, XxxRepository repository),然后应用层在调用该方法时,传入注入好的 repository 实例即可。但是这样的方式,提高了客户端使用的复杂性。

应用层代码模型

image.png

包依赖

    <dependencies>
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-domain</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

应用服务 io.study.order.app.service.OrderAppService

/**
 * 订单应用服务
 */
@Service
public class OrderAppService {
    /**
     * 创建一个订单
     *
     * @param order
     */
    public void createOrder(Order order) {
        /**
         * 存储订单
         */
        order.saveOrder(order);
        /**
         * 扣减库存
         */
    }
}

应用服务用于服务编排,如上述先存储订单,然后再调用库存服务减库存。(库存服务属于第三方服务,第三方服务的集成见下一小节)

基础设施层代码模型

image.png

包依赖

    <dependencies>
        <!-- 领域模块 -->
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-domain</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- mapstruct -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
  1. mapstruct 用于实现模型映射器,关于其使用见 https://www.jianshu.com/p/53aac78e7d60
  2. 数据存储采用 mysql,数据库操作框架使用 mybatis,可以看到,领域层对具体的技术实现并不关注,仅关注业务,通过 DDD 实现了技术细节与业务逻辑的解耦。

资源库实现 io.study.order.repository.impl.OrderRepositoryImpl

/**
 * 订单资源库实现类
 */
@Repository
public class OrderRepositoryImpl implements OrderRepository {
    @Resource
    private OrderDAO orderDAO;

    @Override
    public void add(Order order) {
        orderDAO.insertSelective(OrderDOConverter.INSTANCE.toDO(order));
    }

    @Override
    public Order orderOfId(OrderId orderId) {
        OrderDO orderDO = orderDAO.selectByPrimaryKey(orderId.getId());
        return OrderDOConverter.INSTANCE.fromDO(orderDO);
    }
}

数据库操作接口 io.study.order.data.OrderDAO

/**
 * 订单 DAO
 * 使用 mybatis-generator 自动生成
 */
@org.apache.ibatis.annotations.Mapper
public interface OrderDAO {
    int insertSelective(OrderDO record);
    OrderDO selectByPrimaryKey(Long id);
}

数据库实现类 resources/mapper/OrderDAO.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="io.study.order.data.OrderDAO">
    <resultMap id="BaseResultMap" type="io.study.order.data.OrderDO">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
    </resultMap>
    <sql id="Base_Column_List">
    id, name
    </sql>
    <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long">
        select
        <include refid="Base_Column_List"/>
        from `order`
        where id = #{id,jdbcType=BIGINT}
    </select>
    <insert id="insertSelective" parameterType="io.study.order.data.OrderDO">
        insert into `order`
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="id != null">
                id,
            </if>
            <if test="name != null">
                name,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="id != null">
                #{id,jdbcType=BIGINT},
            </if>
            <if test="name != null">
                #{name,jdbcType=VARCHAR},
            </if>
        </trim>
    </insert>
</mapper>

数据对象

/**
 * 订单数据库对象
 */
@Data
public class OrderDO {
    /**
     * 订单 ID
     */
    private Long id;
    /**
     * 订单名称
     */
    private String name;
}

数据对象转换器 io.study.order.data.OrderDOConverter

/**
 * OrderDO 转换器
 */
@org.mapstruct.Mapper
public interface OrderDOConverter {
    OrderDOConverter INSTANCE = Mappers.getMapper(OrderDOConverter.class);

    @Mapping(source = "id.id", target = "id")
    OrderDO toDO(Order order);

    @Mapping(target = "id", expression = "java(OrderId.of(orderDO.getId()))")
    void update(OrderDO orderDO, @MappingTarget Order order);

    default Order fromDO(OrderDO orderDO) {
        Order order = OrderFactory.createOrder();
        INSTANCE.update(orderDO, order);
        return order;
    }
}

在创建实体对象时,需要使用工厂进行创建,这样才能为实体注入资源库实现。

用户接口层代码模型

image.png

包依赖

    <dependencies>
        <!-- 领域模块 -->
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-domain</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- 应用模块 -->
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-application</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- mapstruct -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- springboot-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- springfox -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
        </dependency>
    </dependencies>

引入 springfox-boot-starter:3.0.0 来实现自动化可测试的 Rest 接口文档

Controller io.study.order.web.OrderController

/**
 * Order 控制器
 */
@Api("订单控制器")
@RestController
@RequestMapping("order")
public class OrderController {
    @Resource
    private OrderAppService orderAppService;
    @Resource
    private OrderRepository orderRepository;

    /**
     * 创建一个订单
     *
     * @param orderDto
     */
    @ApiOperation("创建订单")
    @PostMapping("/create")
    public void createOrder(@RequestBody OrderDto orderDto) {
        orderAppService.createOrder(OrderDtoAssembler.INSTANCE.fromDTO(orderDto));
    }

    /**
     * 查询一个订单
     *
     * @param id 订单ID
     * @return
     */
    @ApiOperation("根据订单ID获取订单")
    @GetMapping("/find/{id}")
    public OrderDto findOrder(@PathVariable Long id) {
        Order order = orderRepository.orderOfId(OrderId.of(id));
        return OrderDtoAssembler.INSTANCE.toDTO(order);
    }
}

数据传输对象 io.study.order.web.dto.OrderDto

/**
 * 订单数据传输对象
 */
@ApiModel("订单")
@Data
public class OrderDto {
    /**
     * 订单 ID
     */
    @ApiModelProperty("订单ID")
    private Long id;
    /**
     * 订单名称
     */
    @ApiModelProperty("订单名称")
    private String name;
}

DTO 转换类 io.study.order.web.assembler.OrderDtoAssembler

/**
 * OrderDTO<=>Order 转换器
 */
@Mapper
public interface OrderDtoAssembler {
    OrderDtoAssembler INSTANCE = Mappers.getMapper(OrderDtoAssembler.class);
    /**
     * DTO 转 Entity
     * @param dto
     * @return
     */
    default Order fromDTO(OrderDto dto) {
        Order order = OrderFactory.createOrder();
        INSTANCE.update(dto, order);
        return order;
    }

    /**
     * Entity 转 DTO
     * @param order
     * @return
     */
    @Mapping(source = "id.id", target = "id")
    OrderDto toDTO(Order order);

    @Mapping(target = "id", expression = "java(OrderId.of(orderDto.getId()))")
    void update(OrderDto orderDto, @MappingTarget Order order);
}

转换器应该写在外层还是内层,比如 OrderDtoAssembler 是应该写在 interfaces 层,还是写在 application 层,从依赖关系来考虑:假设写在 application 层,由于 DTO 是定义在 interfaces 层,那么 application 需要依赖 interfaces,与 外层依赖内层 的原则不符,那么 DTO 是否可以写在 application 层,假设现在有个需要对外提供的 Dubbo 接口,该接口中存在的 DTO 是需要打包给第三方的,所以并不适合写在 application 层。

启动模块代码模型

image.png

包依赖

    <dependencies>
        <!-- 基础设施层 -->
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-infrastructure</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- 用户接口层 -->
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-interfaces</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

启动器 io.study.order.OrderApplication

/**
 * 应用启动器
 */
@EnableOpenApi
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

springfox3.x 通过注解 @EnableOpenApi 来启动自动配置

配置文件 resource/application.properties

mybatis.mapper-locations=/mapper/*.xml

spring.datasource.username: root
spring.datasource.password: xxx
spring.datasource.url: jdbc:mysql://localhost:3306/my-test?useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
上一篇下一篇

猜你喜欢

热点阅读