领域驱动设计实战

---------------------领域驱动(DDD,Domain Driven Design)为软件设计提供了一套完整的理论指导和落地实践,通过战略设计
---------------------

领域驱动(DDD,Domain Driven Design)为软件设计提供了一套完整的理论指导和落地实践,通过战略设计和战术设计,将技术实现与业务逻辑分离,来应对复杂的软件系统。本系列文章准备以实战的角度来介绍 DDD,首先编写领域驱动的代码模型,然后再基于代码模型,引入 DDD 的各项概念,先介绍战术设计,再介绍战略设计。

DDD 实战1 - 基础代码模型
DDD 实战2 - 集成限界上下文(Rest & Dubbo)
> DDD 实战3 - 集成限界上下文(消息模式)
> DDD 实战4 - 领域事件的设计与使用
> DDD 实战5 - 实体与值对象
> DDD 实战6 - 聚合的设计
> DDD 实战7 - 领域工厂与领域资源库
> DDD 实战8 - 领域服务与应用服务
> DDD 实战9 - 架构设计
> DDD 实战10 - 战略设计

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

 
  • 领域层(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 根模块

领域层代码模型

 

 

包依赖

    <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 实例即可。但是这样的方式,提高了客户端使用的复杂性。

应用层代码模型

 

 

包依赖

    <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 层。

启动模块代码模型

 

 

包依赖

    <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

引用第三方接口

首先介绍 ordercenter 做为消费者引用第三方接口的方式(第三方接口分别提供 Rest 和 Dubbo 两种形式),然后介绍 ordercenter 做为服务提供者为第三方提供服务接口的方式。

代码:https://github.com/zhaojigang/ordercenter

设计原则:

  1. 第三方服务的接入需要使用防腐层进行包装,进行防腐设计
  2. 第三方服务由应用服务层进行编排
  3. 第三方服务的实现由基础设施层进行实现
  4. 根据 外层依赖内层 的原则,需要将第三方服务的防腐接口和防腐模型放置在应用服务层,其实现放置在基础设施层;应用层只关心业务逻辑,不关心具体的技术实现(不关心是 Rest 服务还是 Dubbo 服务),基础设施层来关心技术细节。

应用服务层

 

 

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

/**
 * 订单应用服务
 */
@Service
public class OrderAppService {
    @Resource(name = "restInventoryAdaptor")
    private InventoryAdaptor inventoryAdaptor;

    /**
     * 创建一个订单
     *
     * @param order
     */
    public void createOrder(Order order) {
        /**
         * 获取商品库存信息,进行校验
         */
        InventoryDTO inventoryDTO = inventoryAdaptor.getRemainQuality(order.getGoodsId());
        if (inventoryDTO.getRemainQuantity() - order.getBuyQuality() < 0) {
            throw new OrderException(400, "商品库存不足");
        }

        /**
         * 扣减库存
         */
        inventoryAdaptor.reduceRemainQuality(order.getGoodsId(), order.getBuyQuality());

        /**
         * 存储订单
         */
        order.saveOrder(order);
    }
}

第三方服务防腐接口 io.study.order.rpc.inventory.InventoryAdaptor

/**
 * 库存第三方服务接口
 */
public interface InventoryAdaptor {
    /**
     * 获取商品剩余库存信息
     * @param goodsId
     * @return
     */
    InventoryDTO getRemainQuality(Long goodsId);

    /**
     * 扣减库存
     * @param goodsId
     * @param reduceQuality 减少的库存数
     * @return
     */
    Boolean reduceRemainQuality(Long goodsId, Integer reduceQuality);
}

第三方服务防腐模型 io.study.order.rpc.inventory.InventoryDTO

/**
 * 库存 DTO
 */
@Data
public class InventoryDTO {
    /**
     * 商品 ID
     */
    private Long goodsId;
    /**
     * 剩余库存
     */
    private Integer remainQuantity;
}

基础设施层

 

 

 

在基础设施层,使用了 Rest 和 Dubbo 两种方式来实现了库存服务接口。分别来看下实现。

第三方服务实现 io.study.order.rpc.impl.RestInventoryAdaptorImpl

/**
 * 库存服务(Rest 实现)
 */
@Component("restInventoryAdaptor")
public class RestInventoryAdaptorImpl implements InventoryAdaptor {
    private static final CloseableHttpClient HTTP_CLIENT = HttpClientBuilder.create().build();

    @Override
    public InventoryDTO getRemainQuality(Long goodsId) {
        HttpGet httpGet = new HttpGet("http://localhost:8082/inventory/getInventoryInfo?goodsId=" + goodsId);
        try {
            CloseableHttpResponse response = HTTP_CLIENT.execute(httpGet);
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                return RestInventoryTranslator.translateRestResponse2InventoryDTO(EntityUtils.toString(response.getEntity()));
            }
        } catch (IOException e) {
            throw new OrderException(500, "调用库存服务异常, e:" + e);
        }
        return null;
    }

    @Override
    public Boolean reduceRemainQuality(Long goodsId, Integer reduceQuality) {
        HttpPost httpPost = new HttpPost("http://localhost:8082/inventory/reduceRemainInventory?goodsId=" + goodsId + "&reduceQuality=" + reduceQuality);
        try {
            CloseableHttpResponse response = HTTP_CLIENT.execute(httpPost);
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                return RestInventoryTranslator.translateRestResponse2Boolean(EntityUtils.toString(response.getEntity()));
            }
        } catch (IOException e) {
            throw new OrderException(500, "调用库存服务异常, e:" + e);
        }
        return null;
    }
}

第三方服务防腐对象转换器 io.study.order.rpc.impl.RestInventoryTranslator

/**
 * 库存服务类型转换器(Rest)
 */
public class RestInventoryTranslator {
    public static InventoryDTO translateRestResponse2InventoryDTO(String restResponse){
        return JSON.parseObject(restResponse, InventoryDTO.class);
    }

    public static Boolean translateRestResponse2Boolean(String restResponse){
        return JSON.parseObject(restResponse, Boolean.class);
    }
}

库存服务 Rest 实现

/**
 * 库存 rest 服务
 */
@RestController
@RequestMapping("inventory")
public class InventoryController {
    @GetMapping("getInventoryInfo")
    public InventoryInfoDTO getInventoryInfo(@RequestParam("goodsId") Long goodsId) {
        InventoryInfoDTO dto = new InventoryInfoDTO();
        dto.setGoodsId(goodsId);
        if (goodsId == 1L) {
            dto.setRemainQuantity(100);
            dto.setInTransitQuantity(101);
        } else {
            dto.setRemainQuantity(200);
            dto.setInTransitQuantity(202);
        }
        return dto;
    }

    @PostMapping("reduceRemainInventory")
    public Boolean getInventoryInfo(@RequestParam("goodsId") Long goodsId, @RequestParam("reduceQuality") Integer reduceQuality) {
        return true;
    }
}

再来看下 Dubbo 的服务实现方式,Dubbo 的配置方式通常会有两种:xml 和注解方式。这里以注解方式进行演示。首先看库存服务提供的 Dubbo 服务。

库存服务(Dubbo 形式)

/************************* 接口 *************************/
/**
 * 库存服务对外接口
 */
public interface InventoryFacade {
    /**
     * 获取商品库存信息
     */
    InventoryInfoDTO getRemainQuality(Long goodsId);

    /**
     * 扣减库存
     */
    Boolean reduceRemainQuality(Long goodsId, Integer reduceQuality);
}

/************************* 实现 *************************/
/**
 * 库存服务实现
 */

来看下在 ordercenter 引用第三方服务的姿势

第三方服务实现 io.study.order.rpc.impl.DubboInventoryAdaptorImpl

/**
 * 库存服务(Dubbo 实现)
 */
@Component("dubboInventoryAdaptor")
public class DubboInventoryAdaptorImpl implements InventoryAdaptor {
    @DubboReference(version = "1.0.0", group = "product")
    private InventoryFacade inventoryFacade;

    @Override
    public InventoryDTO getRemainQuality(Long goodsId) {
        InventoryInfoDTO inventoryInfoDTO = inventoryFacade.getRemainQuality(goodsId);
        return DubboInventoryTranslator.INSTANCE.toInventoryDTO(inventoryInfoDTO);
    }

    @Override
    public Boolean reduceRemainQuality(Long goodsId, Integer reduceQuality) {
        return inventoryFacade.reduceRemainQuality(goodsId, reduceQuality);
    }
}

第三方服务防腐对象转换器 io.study.order.rpc.impl.DubboInventoryAdaptorImpl

/**
 * 库存服务防腐对象转换器
 */
@org.mapstruct.Mapper
public interface DubboInventoryTranslator {
    DubboInventoryTranslator INSTANCE = Mappers.getMapper(DubboInventoryTranslator.class);
    InventoryDTO toInventoryDTO(InventoryInfoDTO inventoryInfoDTO);
}

基础设层包依赖

        <!-- dubbo -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>

服务启动模块

/************************* io.study.order.OrderApplication *************************/
/**
 * 应用启动器
 *
 * @author jigang
 */

这里将数据库、dubbo 的配置信息都写在了启动模块中,实际上也可以将这些配置写在他们各自使用的地方,比如可以将这些配置都写在 infrastructure 中,同时还可以将这些配置根据功能拆分成不同的配置文件,之后在启动类使用 @PropertySource 进行加载即可。

提供服务给第三方

如果仅提供 Rest 服务,那么当前的用户接口层中的 io.study.order.web.OrderController 即可。但是绝大多数情况下,需要提供类似 Dubbo 的使用方式,打成 Jar 包给第三方使用,为了避免内部逻辑泄露,以及为了打给第三方的包是一个“干净”的包,我们抽出一个单独的模块 order-client 来实现这一目的。

设计原则:

  1. 创建 order-client 模块:仅存储提供给第三方的接口和对象模型
  2. 用户接口层来实现 order-client 中的接口
  3. 领域层中需要使用 order-client 中的查询对象,所以领域层直接依赖 order-client,最终形成如下的依赖关系。

client 模块

 

 

对外接口 io.study.order.facade.OrderFacade

/**
 * 订单服务
 */
public interface OrderFacade {
    /**
     * 查询订单
     */
    List<OrderDTO> getOrderList(OrderQueryRequest request);

    /**
     * 创建订单
     */
    void createOrder(OrderDTO orderDTO);
}

对外接口返回模型 io.study.order.dto.OrderDTO

@Data
public class OrderDTO implements Serializable {
    private static final long serialVersionUID = 8642623148247246765L;
    /**
     * 订单ID
     */
    private Long id;
    /**
     * 订单名称
     */
    private String name;
    /**
     * 商品ID
     */
    private Long goodsId;
    /**
     * 购买数量
     */
    private Integer buyQuality;
}

对外接口请求参数 io.study.order.dto.OrderQueryRequest

/**
 * order 查询请求参数
 */
@Data
public class OrderQueryRequest implements Serializable {
    private static final long serialVersionUID = 3330101115728844788L;
    /**
     * 订单ID
     */
    private Long orderId;
    /**
     * 订单名称
     */
    private String orderName;
}

用户接口层

 

 

/**
 * 订单服务实现
 */
@DubboService(version = "1.0.0", group = "product")
public class OrderFacadeImpl implements OrderFacade {
    @Resource
    private OrderAppService orderAppService;
    @Resource
    private OrderRepository orderRepository;

    @Override
    public List<OrderDTO> getOrderList(OrderQueryRequest request) {
        List<Order> orderList = orderRepository.ordersOfCondition(request);
        return OrderDTOAssembler.INSTANCE.toDTOList(orderList);
    }

    @Override
    public void createOrder(OrderDTO orderDTO) {
        orderAppService.createOrder(OrderDTOAssembler.INSTANCE.fromDTO(orderDTO));
    }
}

Dubbo 服务的配置和启动与上述的库存服务相同,不在赘述,接下来着重看一下 OrderQueryRequest 对象的传输路径。

类:OrderFacade -> OrderFacadeImpl -> OrderRepository -> OrderRepositoryImpl
层:client      -> interfaces      -> domain          -> infrastructure

由于 domain 中要使用到 client 定义的对象,那么 domain 要依赖 client,乍一看,不符合 外层依赖内层 的原则,实际上,在 DDD 分层模型中,是没有 client 这个模块的;另外, 外层依赖内层 原则的目的是为了保证内层的稳定性,这个稳定怎么理解?个人理解为,模块内的代码不随外界技术的变动而变动,例如,将存储从 mysql 换成了 oracle,我们仅需要处理 infrastructure 层即可,其他内层不动;在比如,当前的都是直接穿数据库的,想使用 Cache Aside Pattern 加一层缓存,那么仅需要在 infrastructure 资源库的实现中进行修改即可,内层逻辑不应该动。但是现在如果是业务本身就发生了变化,那么内部的模型除了部分可以使用开闭设计避免变动时,大部分情况下还是要动的,不管是 application 还是 domain 层,client 被 domain 依赖就是这个道理,假设 domain 不依赖 client,那么我们需要在 domain 层也一模一样的设计一个查询模型,然后在用户接口层进行转换即可,这样也是可以实现的,但是必要性是否有,可以考虑一下。