领域驱动(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
本应用的所有流量入口。包括三部分:
- web 入口的实现:包括 controller、DTO 定义、DTO 转化类
- 消息监听者(消费者):包括 XxxListener
- RPC 接口的实现:比如在使用 Dubbo 时,我们的服务需要开放 Dubbo 服务给第三方,此时需要创建单独的模块包,例如 client 模块,包含 Dubbo 接口和 DTO,在用户接口层中,去做 client 中接口的实现以及 DTO 转化类
基础设施层 infrastructure
本应用的所有流量出口。包括:
- 资源库接口的实现
- 数据库操作接口、数据库实现(如果使用mybatis,则包含 resource/*.xml)、数据库对象 DO、DO 转化类
- 中间件的实现、文件系统实现、缓存实现、消息实现 等
- 第三方服务接口的实现
基于 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);
}
- 资源库接口放置在领域层,实现领域对象自持久化,同时实现依赖反转。
- 依赖反转:将依赖关系进行反转,假设 Order 要做自持久化,那么需要拿到资源库的实现 OrderRepositoryImpl 才行,那么 domain 包就要依赖 infrastructure 包,但是这不符合
外层依赖内层
的原则,所以需要进行依赖反转,由 infrastructure 包依赖 domain 包。实现依赖反转的方式就是在被依赖方中添加接口(例如,在 domain 包中添加 OrderRepository 接口),依赖包对接口进行实现(infrastructure 包中对 OrderRepository 进行实现),这样的好处是,domain 可以完全仅关注业务逻辑,不要关心具体技术细节,不用去关心,到底是存储到 mysql,还是 oracle,使用的数据库框架是 mybatis 还是 hibernate,技术细节的实现由 infrastructure 来完成,真正实现了业务逻辑和技术细节的分离- 资源库的命名推荐:对于资源库,推荐面向集合进行设计,即资源库的方法名采用与集合相似的方法名,例如,保存和更新是 add、addAll,删除时 remove、removeAll,查询是 xxxOfccc,例如 orderOfId,ordersOfCondition,复数使用 xxxs 的格式,而不是 xxxList 这样的格式
- 一个聚合具有一个资源库:比如订单聚合中,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);
}
}
工厂的作用:创建聚合。
工厂的好处:
- 创建复杂的聚合,简化客户端的使用。例如 Order 的创建需要注入资源库,订单创建后,可以直接发布订单创建事件。
- 可读性好(更加符合通用语言),比如 对于创建订单,createOrder 就比 new Order 的语义更加明确
- 更好的保证一致性,防止出错,假设创建两个主订单 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 为空");
}
}
}
- 推荐使用强类型的对象作为实体的唯一标识,好处有两个:
a. 用来避免传参混乱,同时提升接口的可读性,例如 xxx(Long orderId, Long goodsId),假设上述接口第一个参数传了 goodsId,第二个传了 orderId,那么编译期是无法发现的,改为 xxx(OrderId orderId, GoodsId, goodsId) 即可避免,同时可读性也较高。
b. 唯一标识中会有一些其他行为方法,如果唯一标识使用弱类型,那么这些行为方法将会泄露在实体中- 唯一标识类是一个值对象,推荐值对象设置为不可变对象,使用 @lombok.Value 标注值对象,既可标识该对象为值对象,也可以是该类变为不可变类。例如,表示后的 OrderId 没有 setXxx 方法。
- 值对象的行为函数都是无副作用函数(即不能影响值对象本身的状态,例如 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;
}
}
- 聚合根是一个特殊的实体,是整个聚合对外的使者,其他聚合与改聚合沟通的方式只能是通过聚合根
- 由于使用工厂来创建 Order,那么 Order 的构造器需要设置为 protected,防止外界直接使用进行创建
- 实体单个属性的校验需要在 setXxx 中完成自校验
- 实体是可变的、具有唯一标识,其唯一标识通常需要设计成强类型
- 聚合中的 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);
/**
* 扣减库存
*/
}
}
应用服务用于服务编排,如上述先存储订单,然后再调用库存服务减库存。(库存服务属于第三方服务,第三方服务的集成见下一小节)
基础设施层代码模型
包依赖
<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>
- mapstruct 用于实现模型映射器,关于其使用见 https://www.jianshu.com/p/53aac78e7d60
- 数据存储采用 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;
}
}
在创建实体对象时,需要使用工厂进行创建,这样才能为实体注入资源库实现。
用户接口层代码模型
包依赖
<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 做为服务提供者为第三方提供服务接口的方式。
设计原则:
- 第三方服务的接入需要使用防腐层进行包装,进行防腐设计
- 第三方服务由应用服务层进行编排
- 第三方服务的实现由基础设施层进行实现
- 根据
外层依赖内层
的原则,需要将第三方服务的防腐接口和防腐模型放置在应用服务层,其实现放置在基础设施层;应用层只关心业务逻辑,不关心具体的技术实现(不关心是 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
来实现这一目的。
设计原则:
- 创建
order-client
模块:仅存储提供给第三方的接口和对象模型- 用户接口层来实现
order-client
中的接口- 领域层中需要使用
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 层也一模一样的设计一个查询模型,然后在用户接口层进行转换即可,这样也是可以实现的,但是必要性是否有,可以考虑一下。