在 DDD 实战1 - 基础代码模型 介绍了 DDD 的基础代码模型,本文来实现与第三方上下文的调用。首先介绍 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);
}
/************************* 实现 *************************/
/**
* 库存服务实现
*/
@DubboService(version = "1.0.0", group = "product")
public class InventoryFacadeImpl implements InventoryFacade {
@Override
public InventoryInfoDTO getRemainQuality(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;
}
@Override
public Boolean reduceRemainQuality(Long goodsId, Integer reduceQuality) {
return true;
}
}
/************************* 库存服务 Dubbo 接口返回值 *************************/
/**
* 库存信息
*/
@Data
public class InventoryInfoDTO implements Serializable {
private static final long serialVersionUID = -7542780056688475990L;
/**
* 商品ID
*/
private Long goodsId;
/**
* 剩余库存
*/
private Integer remainQuantity;
/**
* 在途库存
*/
private Integer inTransitQuantity;
}
/************************* application.properties *************************/
dubbo.application.name=inventory
dubbo.registry.address=multicast://224.5.6.7:1234
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
/************************* pom.xml *************************/
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.8</version>
</dependency>
来看下在 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
*/
@EnableOpenApi
@EnableDubbo // 启动 Dubbo
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
/************************* application.properties *************************/
# 数据库相关
...
# dubbo 共享
dubbo.application.name=ordercenter
dubbo.registry.address=multicast://224.5.6.7:1234
# dubbo 消费者
dubbo.consumer.check=false
这里将数据库、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 层也一模一样的设计一个查询模型,然后在用户接口层进行转换即可,这样也是可以实现的,但是必要性是否有,可以考虑一下。