业务量猛增!如何用DDD避免一场巨大的架构灾难?
前言
一、DDD是什么
我们把领域拆分为多个子域,并通过上下文映射图提取出实体的限界上下文,这一过程就是DDD的战略设计; 而把问题空间的领域、子域概念通过解决方案空间的描述语言实现软件模型,这就是DDD的战术设计。
二、架构演进-六边形架构
三、编码实战
ddd-demo/├── ddd-order-demo-api ------ 用户界面层-REST│ ├── src│ │ └── main│ │ ├── java│ │ │ └── cn│ │ │ └── huolala│ │ │ └── demo│ │ │ ├── DemoApiApplication.java ------ starter│ │ │ └── api│ │ │ └── controller ------ 客户端接口│ │ │ ├── UserController.java│ │ │ └── OrderController.java│ │ └── resources│ │ └── application.yml
ddd-demo/├── ddd-order-demo-application ------ 应用服务层,主要做业务编排,协调领域层做业务聚合│ ├── src│ │ └── main│ │ └── java│ │ └── cn│ │ └── huolala│ │ └── demo│ │ └── application│ │ ├── convertor ------ 模型转换器│ │ │ ├── OrderConvertor.java│ │ │ └── UserConvertor.java│ │ ├── pojo│ │ │ ├── cmd ------ 用户界面层写请求参数│ │ │ │ ├── OrderChangeItemCmd.java│ │ │ │ ├── OrderCreateCmd.java│ │ │ │ └── co ------ cmd的子结构│ │ │ │ └── OrderCreateProductItemCO.java│ │ │ └── query ------ 用户界面层读请求参数│ │ │ │ ├── UserQuery.java│ │ │ │ └── qo ------ query的子结构│ │ │ └── response ------ 用户界面层的响应模型│ │ │ ├── OrderResponse.java│ │ │ └── UserResponse.java│ │ └── service ------ 应用服务层service,业务编排│ │ ├── OrderService.java│ │ └── UserService.java
ddd-demo/├── ddd-order-demo-domain ------ 领域层,承载当前限界上下文的所有业务逻辑│ ├── src│ │ ├── main│ │ │ ├── java│ │ │ │ └── cn│ │ │ │ └── huolala│ │ │ │ └── demo│ │ │ │ └── domain│ │ │ │ ├── convertor│ │ │ │ ├── infra ------ 基础设施相关接口│ │ │ │ │ ├── mq ------ 基础设施-消息事件│ │ │ │ │ │ └── OrderMessageProducer.java│ │ │ │ │ ├── remote ------ 基础设施-远程调用│ │ │ │ │ │ ├── IProductRemoteService.java│ │ │ │ │ │ └── IScheduleRemoteService.java│ │ │ │ │ └── repository ------ 基础设施-资源库│ │ │ │ │ ├── IDeliveryAddressRepository.java│ │ │ │ │ ├── IOrderRepository.java│ │ │ │ │ └── IUserRepository.java│ │ │ │ ├── model ------ 领域模型│ │ │ │ │ ├── order ------ 子域-核心域│ │ │ │ │ │ ├── DeliveryInfo.java│ │ │ │ │ │ ├── LockInventoryResponse.java│ │ │ │ │ │ ├── Order.java ------ 实体│ │ │ │ │ │ ├── OrderDomainEvent.java│ │ │ │ │ │ ├── OrderEvent.java│ │ │ │ │ │ ├── OrderEventTypeEnum.java│ │ │ │ │ │ ├── OrderItem.java│ │ │ │ │ │ ├── OrderStateEnum.java│ │ │ │ │ ├── product ------ 子域│ │ │ │ │ │ └── Product.java│ │ │ │ │ └── user ------ 子域│ │ │ │ │ ├── User.java│ │ │ │ │ └── UserStateEnum.java│ │ │ │ └── service ------ 领域服务│ │ │ │ ├── OrderDomainService.java│ │ │ │ └── ProductDomainService.java
ddd-demo/├── ddd-order-demo-infra-remote ------ 基础设施层-远程服务调用;访问外部限界上下文│ ├── pom.xml│ ├── src│ │ └── main│ │ └── java│ │ └── cn│ │ └── huolala│ │ └── demo│ │ └── remote│ │ ├── client ------ 外部服务接口定义(REST)│ │ │ ├── ProductApiClient.java│ │ │ ├── ScheduleApiClient.java│ │ │ ├── request ------ 外部服务调用请求参数│ │ │ │ ├── BaseRemoteRequest.java│ │ │ │ └── LockInventoryRequest.java│ │ │ └── response ------ 外部服务调用响应体│ │ │ ├── BaseRemoteResponse.java│ │ │ └── ResponseMessageEnum.java│ │ ├── convertor ------ 转换器,用于外部请求参数或响应体与领域模型的转换│ │ │ └── OrderConvertor.java│ │ ├── service ------ 防腐层,实现了领域层Remote接口,所有外部服务的调用入口│ │ │ ├── ProductRemoteService.java│ │ │ └── ScheduleRemoteService.java├── ddd-order-demo-infra-repository ------ 基础设施层-资源库;数据模型与持久化相关│ ├── pom.xml│ ├── src│ │ └── main│ │ ├── java│ │ │ └── cn│ │ │ └── huolala│ │ │ └── demo│ │ │ ├── convertor ------ 转换器,用于数据模型和领域模型的转换│ │ │ │ ├── DeliveryAddressConvertor.java│ │ │ │ ├── OrderConvertor.java│ │ │ │ ├── OrderItemConvertor.java│ │ │ ├── entity ------ 数据模型│ │ │ │ ├── DeliveryAddressEntity.java│ │ │ │ ├── OrderEntity.java│ │ │ │ ├── OrderItemEntity.java│ │ │ └── repository ------ 资源库│ │ │ ├── DeliveryAddressRepository.java│ │ │ ├── OrderRepository.java│ │ │ ├── OrderRepositoryES.java│ │ │ └── mapper│ │ │ ├── DeliveryAddressMapper.java│ │ │ ├── OrderItemMapper.java│ │ │ ├── OrderMapper.java
@RestController@RequestMapping("/order")public class OrderController {@ResourceOrderService orderService;@PostMapping(name = "/create")public OrderResponse create(OrderCreateCmd cmd) {return OrderConvertor.convertToResponse(orderService.createOrder(cmd));}}
@Transactional(rollbackFor = Exception.class)public Order createOrder(OrderCreateCmd cmd) {String orderNo = UUID.randomUUID().toString();//从资源库获取用户Optional<User> userOptional = userRepository.findById(cmd.getUserId());User user = userOptional.orElseThrow(() -> new DemoBusinessException("用户不存在"));//把参数转换为领域模型,可以定义convertor来进行转换List<OrderItem> orderItemList = makeOrderItems(cmd.getProductItems(), orderNo);//商品域-检查库存,product和OrderItem属于两个域,且使用了外部product服务,所以使用领域服务orderDomainService.checkInventoryAndAssembleOrderItems(orderItemList);//配送地址Optional<DeliveryAddress> deliveryInfoOptional = deliveryAddressRepository.findById(cmd.getDeliveryAddressId());DeliveryAddress deliveryAddress = deliveryInfoOptional.orElseThrow(() -> new DemoBusinessException("配送信息不存在"));//创建订单,最好的方式是使用工厂Order order = Order.create(orderNo, deliveryAddress, orderItemList, user.getUserId());//调度域-锁定库存,用到了远程服务,所以放到了领域服务orderDomainService.lockInventory(order);//创建订单orderRepository.createOrder(order);//发布订单创建事件orderMessageProducer.publish(order, OrderEventTypeEnum.INIT);//返回领域模型,由用户界面层决定转为什么样的模型, 从架构层面限制模型滥用和模型滥转return order;}
/*** 检查库存,组装订单项* @param orderItems* @return*/public void checkInventoryAndAssembleOrderItems(List<OrderItem> orderItems) {if (CollectionUtils.isEmpty(orderItems)) {throw new DemoBusinessException("未选择商品");}//从商品服务获取商品信息List<Long> productIds = orderItems.stream().map(OrderItem::getProductId).collect( Collectors.toList());List<Product> productList = productRemoteService.getProductInfos(productIds);if (CollectionUtils.isEmpty(productList)) {throw new DemoBusinessException("未查询到商品信息");}Map<Long, Product> productMap = productList.stream().collect(Collectors.toMap(Product::getProductId, p -> p));//库存校验for (OrderItem item : orderItems) {Product product = productMap.get(item.getProductId());if (product == null)throw new DemoBusinessException("商品[" + item.getProductName() + "]不存在");if (product.getInventoryCount() < item.getCount())throw new DemoBusinessException("商品[" + product.getProductName() + "]库存不足");//组装订单项信息item.setPrice(product.getPrice());item.setProductName(product.getProductName());}}/*** 锁定库存** @param order*/public void lockInventory(Order order) {Optional<LockInventoryResponse> lockInventoryDTOOptional = scheduleRemoteService.lockInventory(order);LockInventoryResponse lockInventoryResponse = lockInventoryDTOOptional.orElseThrow(() -> new DemoBusinessException("库存锁定失败"));if (lockInventoryResponse.getLockEndTime().before(new Date())) {throw new DemoBusinessException("库存锁定失败");}order.setLockInventoryEndTime(lockInventoryResponse.getLockEndTime());}
//……省略其他方法/*** 创建订单* @param orderNo* @param deliveryAddress* @param orderItemList* @param userId* @return*/public static Order create(String orderNo, DeliveryAddress deliveryAddress, List<OrderItem> orderItemList, Long userId) {Order order = new Order();order.setOrderNo(orderNo);order.setDeliveryAddress(deliveryAddress);order.setOrderItemList(orderItemList);order.setUserId(userId);order.initialize();return order;}//订单状态初始化public void initialize() {this.setStatus(OrderStateEnum.INIT.getValue());this.setOrderTime(new Date());calculateTotalPrice();}public void setTotalPrice(Long totalPrice) {if (Objects.isNull(totalPrice) || totalPrice < 0)throw new DemoBusinessException("Illegal totalPrice");this.totalPrice = totalPrice;}public void setOrderNo(String orderNo) {if (StringUtils.isBlank(orderNo))throw new DemoBusinessException("orderNo can not be null");this.orderNo = orderNo;}//……省略其他方法
public class ProductRemoteService implements IProductRemoteService {@ResourceProductApiClient productApiClient;@Overridepublic List<Product> getProductInfos(List<Long> productIds) {BaseRemoteResponse<List<Product>> response = productApiClient.getProductInfos(productIds);if (response == null || response.failed()) {log.error("getProductInfos error,request:{},response:{}", productIds, response);return Collections.emptyList();}return response.getData();}}
@Repositorypublic class OrderRepository implements IOrderRepository {@ResourceOrderMapper orderMapper;@ResourceOrderItemMapper orderItemMapper;@Resourceprivate RedisTemplate<String, Object> redisTemplate;@Resourceprivate ElasticsearchTemplate<OrderEntityES, Long> orderESTemplate;public Optional<Order> findById(long orderId) {return Optional.ofNullable(OrderConvertor.convertToDO(elasticsearchTemplate.getById(orderId,OrderEntity.class)));}/*** 创建订单* @param order*/@Overridepublic void createOrder(Order order) {//保存订单OrderEntity orderEntity = OrderConvertor.convertToEntity(order);orderMapper.insert(orderEntity);order.setOrderId(orderEntity.getId());//保存订单项List<OrderItem> orderItemList = order.getOrderItemList();orderItemList.forEach(orderItem -> {orderItem.setOrderId(orderEntity.getId());orderItemMapper.insert(OrderItemConvertor.INSTANCT.convertToEntity(orderItem));});}}
四、货拉拉用户CRM改造实践
几个月后,你自己再次看到这段代码时,是否能够马上理解? 别人接手你的代码时是否容易看得懂? 在新增需求时,这段代码是否容易改,是否容易测试? 在其依赖发生变化时,这段代码能否快速的响应变化?响应变化的成本有多少?
可扩展性:向六边形架构转变,提升系统的扩展性和灵活性 稳定性:通过业务编排,降低业务代码的复杂度,提升系统的可维护性,降低稳定性风险 交付效率:使业务代码更加易于理解,提高团队的协作效率,缩短开发周期,从而加快功能交付速度,满足业务快速发展的需求。
不影响已有业务逻辑 重构风险较小
重构期间模型混乱 依赖功能迭代,时间跨度长
从层级中提取出domain抽象,已有代码逻辑逐步由细节调用改为抽象调用; 应用依赖倒置反转层级结构。
重构期间架构模型变动平滑 开发人员容易接受 重构风险较小
时间跨度较大
抽象空的domain层 domain中定义IxxxRepository接口 DAO实现相应的Repository接口, 并应用依赖倒置反转层级关系 service中引用DAO的地方改为IRepository接口
public MerchantBasicResponse createVisit(CreateVisitRequest request) {EmployeeInfo info = EmployeeUtils.get();//……省略部分代码// 商户验证,存在才能新增拜访MerchantEntity merchant = merchantBaseValidator.checkMerchantExist(request.getMerchantId());// 只能拜访本人的商户if (!Objects.equals(info.getHllerId(), merchant.getOwnerId())) {log.warn("owner not current user, merchant: {}", merchant);throw new ForceException(ErrorCodeEnum.OWNER_NOT_YOU);}//拜访定位异常判定if (Objects.nonNull(merchant.getLongitude()) && Objects.nonNull(merchant.getLatitude())) {double distance = ForceDistanceUtil.distance(merchant.getLongitude().doubleValue(),merchant.getLatitude().doubleValue(), request.getLongitude().doubleValue(), request.getLatitude().doubleValue());long maxVisitDistance = LaLaConfigClient.getAppLongConfigValue(MAX_VISIT_DISTANCE_KEY, DEFAULT_MAX_VISIT_DISTANCE);log.info("visit distance: {}", distance);// ……省略部分代码}// 张贴海报参数验证VisitValidator.validateActivityType(request);// 物资校验validateMaterialDelivery(request);// 坐标更新if (Objects.nonNull(merchant.getDataType()) && merchant.getDataType() == 2&& Objects.nonNull(merchant.getRelocation()) && Objects.equals(Boolean.FALSE, merchant.getRelocation())) {merchant.setLongitude(request.getLongitude());merchant.setLatitude(request.getLatitude());merchant.setRelocation(true);merchant.setLocation(request.getAddress());merchantDao.updateById(merchant);abnormalVisitPosition = false;}// 创建拜访实体VisitEntity visit = VisitConvert.convertToEntity(request);visit.setRegionId(merchant.getRegionId());visit.setCityId(merchant.getCityId());visit.setMarketId(merchant.getMarketId());visit.setOpName(info.getUserName());//密集拜访LocalDateTime nowTime = LocalDateTime.now();//往前推最大间隔int minVisitCommitInterval = LaLaConfigClient.getAppIntConfigValue(MIN_VISIT_COMMIT_INTERVAL_KEY, DEFAULT_MIN_VISIT_COMMIT_INTERVAL);LocalDateTime deadline = nowTime.minusSeconds(minVisitCommitInterval);List<Visit> latestVisitList = visitDao.getLatestVisitList(info.getAccount(), deadline);//密集拜访判定intensiveVisit = CollUtil.isNotEmpty(latestVisitList);visitDao.insert(visit);// 商户状态校验boolean isBuilding = Objects.equals(MerchantStatusEnum.BUILDING.getStatus(), merchant.getStatus());if (isBuilding) {VisitValidator.validateFirstVisit(request);MerchantEntity entity = MerchantConvert.convertToCompleteEntity(request, visit.getVisitTime());merchantDao.updateById(entity);List<MerchantPictureEntity> list = MerchantConvert.convertToMerchantPictureEntity(request);merchantPictureService.saveBatch(list);} else {MerchantEntity entity = new MerchantEntity();entity.setId(request.getMerchantId());entity.setLastVisitTime(visit.getVisitTime());merchantDao.updateById(entity);}// 商户操作日志List<MerchantOperateLogEntity> logs = new ArrayList<>();//处理拜访质检标签saveInterviewee(request, logs, visit, merchant, abnormalVisitPosition, intensiveVisit);//拜访照片处理List<VisitPictureEntity> pictures = new ArrayList<>();pictures.addAll(convertToVisitPicture(visit.getId(), request.getFacadePictures(), PictureTypeEnum.FACADE));pictures.addAll(convertToVisitPicture(visit.getId(), request.getIndoorPictures(), PictureTypeEnum.INDOOR));if (null != request.getPosterPictures()) {pictures.addAll(convertToVisitPicture(visit.getId(), request.getPosterPictures(), PictureTypeEnum.POSTER));}visitPictureDao.batchInsert(pictures);// 商户建档审批流程if (isBuilding) {//……省略部分代码}// 保存商户操作日志merchantOperateLogService.saveBatch(logs);// 统计新增拜访数MetricMonitor.monitor(MetricEnum.VISIT_CREATED_NUM_EVERY_DAY);// 商户拜访跳转到商户列表页,返回商户变更信息MerchantBasicResponse changeInfo = new MerchantBasicResponse();changeInfo.setMerchantId(merchant.getId());// 商户自动踢公时间changeInfo.setAutoReleaseTime(merchant.getAutoReleaseTime());// 今日已拜访changeInfo.setHasVisitToday(Boolean.TRUE);if (isBuilding) {// 建档中,商户状态变更changeInfo.setStatus(MerchantStatusEnum.APPROVAL.getStatus());// 用车情况changeInfo.setCarUsage(request.getCarUsage());//……省略部分代码}// 拜访计划完成。visitPlanService.completeVisitPlan(LocalDate.now(), info.getAccount(), request.getMerchantId());return changeInfo;}
public MerchantAppResponse createVisit(CreateVisitRequest request) {// 商户私海检查MerchantEntity merchant = merchantDomainService.checkMerchantIsPrivate(request.getMerchantId());// 建档状态boolean isBuilding = Objects.equals(MerchantStatusEnum.BUILDING.getStatus(), merchant.getStatus());// 首次建档校验VisitValidator.validateFirstVisit(merchant, request);// 海报活动校验VisitValidator.validateActivityType(request);// 物资校验materialDomainService.validateMaterialDelivery(request);// 创建拜访VisitEntity visit = VisitConvert.convertToEntity(request);// 保存拜访照片visitDomainService.saveVisit(visit, request);// 记录商户操作日志,批量保存List<MerchantOperateLogEntity> operateLogList = new ArrayList<>();// 保存拜访用户相关信息,并记录日志visitDomainService.saveVisitUserInfo(request, visit, merchant, operateLogList);// 装配拜访物资发放信息,并记录日志List<VisitUserMaterialEntity> visitUserMaterialList = visitDomainService.assembleVisitMaterialsAndRecordLog(request, visit.getId(), merchant, operateLogList);// 处理物资发放materialDomainService.handleVisitMaterialDelivery(visitUserMaterialList, visit);//更新拜访后的商户信息merchantDomainService.updateMerchantInfoWithVisitAndRecordLog(merchant, visit, request, operateLogList);// 拜访时间List<Long> userIdList = request.getInterviewees().stream().map(IntervieweeVO::getUserId).collect(Collectors.toList());userRepository.updateLastVisitTime(userIdList, request.getVisitTime());// 构造responseMerchantAppResponse response = MerchantAppResponse.createBaseResponse(merchant.getId(),merchant.getAutoReleaseTime(), true);// 保存商户操作日志merchantOperateLogRepository.saveBatch(operateLogList);// 拜访计划visitPlanRepository.completeVisitPlan(LocalDate.now(), EmployeeUtils.get().getAccount(), request.getMerchantId());// 埋点MetricMonitor.monitor(MetricEnum.VISIT_CREATED_NUM_EVERY_DAY);return response;}
一个聚合根对应一个domain service(如果需要); 单方法超过xx行代码(以实际业务场景评估,建议100行以上)必须用domain service进行编排; 一个service的方法操作xx个(建议2个)以上不同的聚合(或实体)或存在外部上下文依赖时,必须使用domain service; 抽象到domain service的方法,原则上每个方法代表一个聚合的一系列逻辑处理和一个聚合(或实体)的数据库写操作; 每个实体在整个方法的生命周期中只写1次,防止逻辑跳跃和逻辑隐匿。
domain中定义IxxxRemote接口 远程调用类实现相应的IRemote接口, 应用依赖倒置反转层级关系 domain中注入原远程调用类的地方改为IRemote接口
简单性:简化了系统复杂性,模型边界也更加清晰。 可演化性:降低研发人员的变更对开发效率的影响,使所有人都可以轻松理解并安全地修改系统,提高团队内的研发资源利用率 可扩展性:系统扩展性更高,节省更换或新增底层中间件的开发成本约50%以上
五、总结
DDD没有统一标准,设计要结合自身实际业务 不要太局限于概念,找到最适合团队的实践方式 不忘初心,记得为什么要DDD
使用领域服务避免平级调用
领域模型和数据模型解耦好处:
六边形架构的好处:
关于防腐层