ddd新车上路

ddd新车上路

年前领导力荐ddd,认为是很不错的架构思路。笔者学习了一段时间,尝试重构了部分代码,折腾出了几个线上bug,终于换回了宝贵的经验,这里和大家分享一下。

传统开发常见的设计思路

常规接口,一般简单分成查询和执行两种,查询接口正常无副作用,pass。我们谈谈有副作用的。
比方说,创建订单,更新状态这两个场景,开发接到这个需求接下来的动作大概是这样:
1 分析有多少表要建,多少字段,字段的业务含义是啥。
2 工具反向生成代码
3 定一下完成服务从到到位大致的业务主流程,写几个空方法
4 填充,调试

稍微归纳一下,这里有两个问题:
1 基于数据库编程,并且基于副作用编程。
2 本质上,其实回归到了面向过程的编程。

填充代码阶段,我们思路很自然会变成,应该set哪个字段哪个值,123,完事提交。这样撸出来的代码,其实就是思路是自然语言,不是领域思路,标准的面向过程编程

不能说这种开发/设计方式就很大问题,在解决复杂程度相对较低的场景,是相当合适的。再次引用我司架构大佬的原话:架构的本质是平衡

何谓DDD

DDD,即便是领域驱动设计,先来几个重要概念。
DP,引用阿里大佬原文

Domain Primitive是一个在特定领域里,拥有精准定义的,可自我验证的,拥有行为的value object。

简单说,VO的升级版本,可以完成对自己属性的校验。DP要求无副作用。实际使用起来,效果一般,应用场景其实不广泛,主要是需要校验的字段,可以聚合校验到DP。

Data Object:数据对象,orm反向生成过来的,和表一一对应,唯一作用,解决存储问题,通过DAO层解耦和DB的交互,业务操作不应当直接操作DO(查询方法除外)。

Entity:业务实体,方法和字段命名,应当和业务语言一致(产品能看懂),entity根据实际业务搭建,最简单的场景可能只是和DO一样,复杂场景可能封装多个不同do对象,聚合多对象行为。

Data Transfer Object(DTO):数据传输对象,一半是APP层的入参,代表接口协议。要求无副作用。

来个范例:用上面订单的场景,一个订单,对应一个订单头,多个商品行,多有优惠行。

DTO:入参
DO层:3张表对应3个DO
Entity层:3个基础的entity,对应3个DO,一个聚合的entity,订单对象,对订单的业务操作,由该对象提供方法,在订单对象这个Eitity中完成优惠匹配拆分,验证,转换状态等操作。最终再由Repository层完成和DB交互。

示例

这是一个还没彻底调整完的代码,业务是换货(退旧商品,换新商品,即同时产生新客退单,新销售单),里面部分直接使用了do,魔法数字请忽略。

public ExchangeOrder submitOrder(QueryPayReq req) {ExchangeOrder result = new ExchangeOrder();ExchangeOrderEntity entity = new ExchangeOrderEntity();//加载换货申请单entity.setApply(saleOrderReturnFullRepository.loadByVposOrderNum(req.getVposOrderNum()));entity.setApplyPays(vposPayReturnDao.queryByVposOrderNum(req.getVposOrderNum()));SaleOrderReturnEntity apply = entity.getApply().getOrder();//加载换货新单entity.setNewSale(saleOrderFullRepository.loadByOrderNum(apply.getExchangeNewOrderSn()));entity.setSkuMap(itemService.query4Pay(apply.getCompanyCode(), apply.getXstoreId(), entity.getBarcodes()));result.setSaleOrder(BeanMapper.map(entity.getNewSale().getOrder(), SaleOrderDto.class));result.setRefundOrderHead(BeanMapper.map(apply, com.vip.fcs.vpos.service.sale.order.obj.SaleOrderReturn.class));result.setSaleOrder(BeanMapper.map(entity.getNewSale().getOrder(), SaleOrderDto.class));result.setRefundOrderHead(BeanMapper.map(apply, com.vip.fcs.vpos.service.sale.order.obj.SaleOrderReturn.class));//校验换货是否合法if (!entity.valid()) {result.setCode("201");result.setMsg("换货校验不通过");return result;}//计算新单优惠entity.calcNewActive();//复制支付行entity.copyNewPays();//调整新单同款商品金额entity.changeSameSnPrice();//更新状态entity.updateStatus();//保存exchangeOrderRepository.saveConfirm(entity);//发送订单成单消息Vms2Utils.publishVmsMsg("channel.fcs2.vpos.order.exchange.event",entity.getApply().getOrder().getVposOrderNum());result.setCode("200");result.setMsg("成功");return result;}

DTO :
QueryPayReq 入参
ExchangeOrder 返回

Entity:
ExchangeOrderEntity 换货订单对象,我们可以看到,换货对象的业务操作,都聚合到ExchangeOrderEntity中。

        //校验换货是否合法entity.valid();//计算新单优惠entity.calcNewActive();//复制支付行entity.copyNewPays();//调整新单同款商品金额entity.changeSameSnPrice();//更新状态entity.updateStatus();

以上的方法,都是业务语言。
最后由

        //保存exchangeOrderRepository.saveConfirm(entity);

Repository完成和DB交互。
ExchangeOrderEntity的代码

@Data
public class ExchangeOrderEntity {private SaleOrderFullEntity newSale;private List<VposPay> newPays;private SaleOrderReturnFullEntity apply;private List<VposPayReturn> applyPays;private Map<String, ProductSku> skuMap;private boolean isSameSn = false;public boolean valid() {//不含优惠的场景,支持换同价或者同价if (CollectionUtils.isEmpty(apply.getActives())) {BigDecimal newPriceSum = newSale.getItems().stream().map(p -> p.getAmount().getAmount().multiply(p.getPrice())).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);BigDecimal applySum = apply.getItems().stream().map(p -> p.getAmount().multiply(p.getPrice())).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);return newPriceSum.compareTo(applySum) == 0;}//同款场景Map<String, BigDecimal> applySnMap = apply.getItems().stream().collect(Collectors.toMap(k -> {ProductSku productSku = skuMap.get(k.getBarcode());return productSku.getSn();}, SaleOrderItemReturnEntity::getAmount, BigDecimal::add));Map<String, BigDecimal> saleSnMap = newSale.getItems().stream().collect(Collectors.toMap(k -> {ProductSku productSku = skuMap.get(k.getBarcode());return productSku.getSn();}, v -> v.getAmount().getAmount(), BigDecimal::add));isSameSn = applySnMap.entrySet().stream().allMatch(p -> p.getValue().compareTo(saleSnMap.getOrDefault(p.getKey(), BigDecimal.ZERO)) == 0)&& applySnMap.size() == saleSnMap.size();return isSameSn;}public Set<String> getBarcodes() {Set<String> applyBarcodes = apply.getItems().stream().map(SaleOrderItemReturnEntity::getBarcode).collect(Collectors.toSet());Set<String> newBarcodes = newSale.getItems().stream().map(SaleOrderItemEntity::getBarcode).collect(Collectors.toSet());if (CollectionUtils.isNotEmpty(newBarcodes)) {applyBarcodes.addAll(newBarcodes);}return applyBarcodes;}public void calcNewActive() {//换货申请单不含优惠,则新单也不需要分摊优惠if (CollectionUtils.isEmpty(apply.getActives())) {return;}//匹配新单商品与旧单商品,找到对应关系Map<String, List<SaleOrderItemReturnEntity>> applySnSkuMap = apply.getItems().stream().collect(Collectors.groupingBy(k -> {ProductSku productSku = skuMap.get(k.getBarcode());return productSku.getSn();}));Map<String, List<SaleOrderItemEntity>> newSaleSnSkuMap = newSale.getItems().stream().collect(Collectors.groupingBy(k -> {ProductSku productSku = skuMap.get(k.getBarcode());return productSku.getSn();}));Map<String, Map<String, BigDecimal>> mapMap = new HashMap<>();for (Map.Entry<String, List<SaleOrderItemReturnEntity>> entry : applySnSkuMap.entrySet()) {List<SaleOrderItemReturnEntity> value = entry.getValue();Map<String, BigDecimal> applySkuMap = value.stream().collect(Collectors.toMap(SaleOrderItemReturnEntity::getBarcode, SaleOrderItemReturnEntity::getAmount, (a, b) -> a));List<SaleOrderItemEntity> itemEntities = newSaleSnSkuMap.get(entry.getKey());
//			List temp = new ArrayList<>();
//			Collections.copy(temp, itemEntities);for (Map.Entry<String, BigDecimal> tempEntry : applySkuMap.entrySet()) {mapMap.put(tempEntry.getKey(), split(tempEntry.getValue(), itemEntities));}}//复制并生成优惠行for (SaleOrderActiveReturnEntity applyActive : apply.getActives()) {List<GoodsDiscountDetail> goodsDiscountDetails = JsonUtil.toList(applyActive.getGoodsDiscountDetails(), GoodsDiscountDetail.class);List<GoodsDiscountDetail> newDetails = goodsDiscountDetails.stream().flatMap(p -> {List<GoodsDiscountDetail> tempList = new ArrayList<>();Map<String, BigDecimal> newSkuMap = mapMap.get(p.getBarcode());for (String barcode : newSkuMap.keySet()) {tempList.add(new GoodsDiscountDetail(p.getFavMoney(), barcode));}return tempList.stream();}).collect(Collectors.toList());SaleOrderActiveEntity active = new SaleOrderActiveEntity();BeanUtil.copy(applyActive, active);active.setVposOrderNum(newSale.getOrder().getVposOrderNum());active.setOrderNum("");active.setId(null);active.setGoodsDiscountDetails(JsonUtil.toStr(newDetails));newSale.getActives().add(active);}}public void copyNewPays() {if (CollectionUtils.isEmpty(applyPays)) {return;}newPays = new ArrayList<>();for (VposPayReturn applyPay : applyPays) {VposPay pay = new VposPay();BeanUtil.copy(applyPay, pay);pay.setVposOrderNum(newSale.getOrder().getVposOrderNum());pay.setOrderNum("");pay.setStatus(PayCommonService.ORDER_PAY_STATUS_EXCHANGE);pay.setId(null);newPays.add(pay);}}public void changeSameSnPrice(){if (!isSameSn) {return;}Map<String, BigDecimal> applySnPriceMap = apply.getItems().stream().collect(Collectors.toMap(k -> {ProductSku productSku = skuMap.get(k.getBarcode());return productSku.getSn();}, SaleOrderItemReturnEntity::getPrice, (a,b)->a));for (SaleOrderItemEntity item : newSale.getItems()) {ProductSku productSku = skuMap.get(item.getBarcode());BigDecimal applyPrice = applySnPriceMap.getOrDefault(productSku.getSn(),BigDecimal.ZERO);if(applyPrice.compareTo(BigDecimal.ZERO) != 0){item.setPrice(applyPrice);}}}public void updateStatus() {newSale.getOrder().setStatus(OrderStatus.SUBMITTING.getStatus());apply.getOrder().setStatus(OrderStatus.SUBMITTING.getStatus());}private Map<String, BigDecimal> split(BigDecimal skuAmount, List<SaleOrderItemEntity> itemEntities) {Map<String, BigDecimal> result = new HashMap<>();for (SaleOrderItemEntity itemEntity : itemEntities) {BigDecimal cur = itemEntity.getAmount().getAmount();if (cur.compareTo(skuAmount) >= 0) {result.put(itemEntity.getBarcode(), skuAmount);cur = cur.subtract(skuAmount);skuAmount = BigDecimal.ZERO;} else if (cur.compareTo(BigDecimal.ZERO) > 0) {result.put(itemEntity.getBarcode(), cur);skuAmount = skuAmount.subtract(cur);cur = BigDecimal.ZERO;}itemEntity.getAmount().setAmount(cur);if (skuAmount.compareTo(BigDecimal.ZERO) == 0) {return result;}}return result;}

entity有自己的状态,数据,有自己的行为,和业务语言一致,是不是很有面向对象的感觉?这个和对比传统基于数据库,基于副作用编程的最大差别,关注的是行为,内聚,而不是过程。

DO忽略。

总结

撸了这么多年代码,感悟还是有点的,所有事情,都要抓住本质。很幸运,撸代码这行的最高境界还是有标准的,高内聚,低耦合,其他都只是方法论和为了达到最终目的的手段。
设计模式可以,ddd可以,具体场景具体分析,没有万能的方法论,只有牛逼的码农。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部