一、背景

1.1 背景说明

之前群里有人分享基于贫血模型和充血模型相关的一些代码实战,同时也有一些小伙伴不太理解这些模型之间的真正内容,本文将通过一个扣库存的代码工程实践来阐述不同视角下的扣库存逻辑的实现,当然在阅读本文之前可以看一下各种模型的相关具体介绍:https://www.cnblogs.com/yinhaiming/articles/1564843.html
当然文章已经比较早了,就目前而言面向spring编程将让我们更少的关注这些模型之间的差异,所以近年来对这些模型的讨论热度已经慢慢降下来了。

1.2 扣库存需求说明

这里对扣库存的逻辑和需求做进一步的介绍,避免出现理解不一致,另外也了体现一定的业务复杂度,需求如下:

  1. 通过controller层发送扣库存请求

  2. 在service中处理扣库存逻辑,判断库存记录是否存在,存在则执行扣库存逻辑,不存在返回扣库存失败

  3. 扣成功之后需要做以下三件事情:

    1.  记录库存变更日志
    2.  更新库存缓存
    3. 发送库存变更记录
    
  4. 整体扣库存逻辑完成之后需要返回是否扣成功

二、常规视角(贫血模型)

2.1 模型介绍

我们先看一下常规视角下的业务模型,这里的业务对象有两个,如下:

2.2 service接口说明

2.3 常规视角下的service实现

    @Overridepublic boolean deduct(String stockCode, Integer deductCount) {//这是常规视角下的扣减库存逻辑,同时也是贫血模型的一个标准demo,但是需要说明的是在贫血模型里操作领域模型就是也在操作//数据模型,所以这个逻辑里面的todo convert如果实现的话可能只是一个翻版的贫血模型实现,但是好在我们已经把数据库模型//从实体模型里分离了出来//1. 检查库存记录是否存在StockDO stockDO = stockMapper.getByStockCode(stockCode);if(stockDO  == null){return false;}//这里可以进行stockDO ---> stockBO操作//2. 重新计算库存数量if(stockDO.getQuantity() < deductCount){return false;}int oldQuantity = stockDO.getQuantity();int newQuantity = stockDO.getQuantity() - deductCount;stockDO.setQuantity(newQuantity);//3. 构建库存变更记录//todo StockRecordBO--> StockRecoredDOStockRecordDO stockRecoredDO = new StockRecordDO();stockRecoredDO.setStockCode(stockCode);stockRecoredDO.setOperationCode(StockOperationEnum.DEDUCT.getCode());stockRecoredDO.setBeforeQuantity(oldQuantity);stockRecoredDO.setAfterQuantity(newQuantity);stockRecoredDO.setOperationDate(new Date());//4. 持久化库存 和变更记录stockRecordMapper.insert(stockRecoredDO);//stockMapper.deduct(stockCode,deductCount);stockMapper.updateQuantity(stockCode,newQuantity);//5. 更新缓存stockCacheService.deduct(stockCode,deductCount);//6. 发送mqstockMqSerivce.sendStockChangeMq(StockChangeEvent.builder().changeQuantity(deductCount).opearationCode(StockOperationEnum.DEDUCT.getCode()).build());return true;}

三、领域建模视角(充血模型)

3.1 失血模型/贫血模型/充血模型的略微区别

  1. 失血模型

domain object只有属性的getter/setter方法的纯数据类。这里需要注意的是上面文章中的domain object也可以直接被持久化层引用,相当于domain object有两个含义,一个是算业务对象一个算持久层数据模型,那在引入了分层架构等因素之后,我们现在讲的是业务对象,所以这里需要有点区别说明。

  1. 贫血模型

domain ojbect包含了不依赖于持久化的领域逻辑。由于我们把一些不依赖外部服务实现的逻辑放到了domain object中,所以这里就有点贫血的意思,但是并不是说所有不依赖持久化的逻辑都放到domain object也算贫血模型,因为这样的话可能算做充血模型了。在现在的复杂技术体系下关于缓存和mq还有数据转换等操作都将不太算做持久化的内容。

  1. 充血模型

充血模型和第二种模型差不多,所不同的就是如何划分业务逻辑。这里的划分逻辑在文中是要表达让service如同controller层一样只是一层壳而已,那么大多数业务逻辑将被domain object持有。所以在一般实践上我们不会太纠结于是贫血模型还是充血模型。因为模型一旦有了更丰富的行为那终将不太好分辨充血模型,或者大多数情况下如果实现了充血模型的话,我们的业务代码可能会有两层壳,不太符合复杂业务的开发习惯。

所以这里总结一下,如果domain object跟数据库实体分离的话,那么数据库实体一般就是失血模型了,当如果domain object也是失血模型,的话对于service来说可能就是过程式的面条型业务逻辑。所以比较好的是在分离的情况下让dto和数据库实体作为失血模型或者贫血模型存在,让domain object作为弱充血模型存在。那么在整个复杂service层下我们会有更多的可操作性来更快的迭代和重构。同时我们不需要纠结于概念是怎么样的,去执着于概念的标准实现。

3.2 领域模型下的StockBO

从上面的介绍来看,贫血模型和充血模型的界限不是特别明确,当然就跟喝酒一样,喝少了脸上不咋红,喝晕了就红扑扑的了,喝醉了可能要送医了。那现在我们看一下领域建模视角下的StockBO对象内容:

3.3 领域服务视角下的service实现

    @Overridepublic boolean deduct(String stockCode, Integer deductCount) {StockDO stockDO = stockMapper.getByStockCode(stockCode);//在业务BO命名StockDDDBO stockDDDBO = StockDDDBO.convertFromDO(stockDO);//1. 检查库存记录是否存在if(stockDDDBO == null){return false;}//2. 重新计算库存数量,这里是区别于常规方式的一个明显标志,我们把扣减行为让stockDDDBO本身去触发,也就是说//这是他自己的事情,他有能力处理int afterQuantity = stockDDDBO.deduct(deductCount);//3. 构建库存变更记录//这里这一步可能会引起争议,如果场因为场景比较多而把大多数转换工作都放到了StockDDDBO上那么可能会导致领域模型变得混乱//比如混淆或者模糊了领域实体本身可以表达的事情,举个例子,我做饭了,花了一些时间,我自己知道,但是记录这件事情的可能是旁边的//女朋友或者说是摄像头,当然这里需要意识到如果是复杂项目就需要考虑是否用mapstruct专门构建转换层了StockRecordBO stockRecordBO = stockDDDBO.buildRecord(StockOperationEnum.DEDUCT, afterQuantity);//4. 持久化库存 和变更记录stockRecordMapper.insert(stockRecordBO.convertToDO());stockMapper.updateQuantity(stockCode,afterQuantity);//注意5,6两步一般来说如果没有其他因素干扰这两步可能需要在事务完成之后走异步事件来进行解耦,//通常来讲扣库存在真实场景下会引起数据库和缓存的不一致问题,这里不过多讨论相关细节,但是总体来说//扣库存的业务操作应该算领域服务里的操作,所以至于怎么具体实现跟一致性需求有关的逻辑还需要进一步构建//业务代码的执行流程。//5. 更新缓存cacheService.deduct(stockCode,afterQuantity);//6. 发送mq,这个发送mq的行为可能不会像上面那样会有更多的讨论,一般来说事情执行完之后总需要有消息的,至于// 异步的还是同步的,或者说是等待事务成功的都相对比较好处理,但是我们需要注意的一点是业务BO需要如何把相关上下文// 参数传递给EventstockMqSerivce.sendStockChangeMq(StockChangeEvent.builder().changeQuantity(deductCount).opearationCode(StockOperationEnum.DEDUCT.getCode()).build());return true;}

3.4 思维导图

四、领域服务视角(涨血模型)

上面见识过了前几种模型之后我们看一下最后一位,涨血模型。文中的描述是取消了service只保留domain object和dao。也就是说web请求过来我们可以直接通过domain object来操作dao等底层基础服务。那我们就看一下这个涨血模型是啥样的。

4.1 第一版本

上面可以看出,如果要实现涨血模型的话,那就需要练就一身吸功大法,将常规视角下的依赖业务对象都吸入自己体内,然后整个逻辑自然而然的就内化为自己的能力了。所以在HighBloodStockServiceImpl的实现里就成了一个壳。

4.2 第二版本

现在因为有了spring阻挠使用吸功大法实现涨血模型,那就需要结合spring的容器特性来化解因为吸了太多业务服务对象导致的真气冲撞。所以一般情况下我们可以通过SpringContextUtils来获取我们想要的bean对象,那么在第二版本的时候我们可以借助spring容器来解决这个问题,所以4.1上面的那些业务服务对象都只需要在用到的时候从SpringContextUtils获取,这里看一下StockHighBloodV2BO业务对象下的boolean exeDeduct()方法的具体实现:

/*** 执行扣减逻辑的入口* @param deductCount* @return*/public boolean exeDeduct(Integer deductCount){StockMapper stockMapper = SpringApplicationContext.getBean(StockMapper.class);StockDO stockDO = stockMapper.getByStockCode(stockCode);//在业务BO命名StockDDDBO stockDDDBO = StockDDDBO.convertFromDO(stockDO);//1. 检查库存记录是否存在if(stockDDDBO == null){return false;}//2. 重新计算库存数量,这里是区别于常规方式的一个明显标志,我们把扣减行为让stockDDDBO本身去触发,也就是说//这是他自己的事情,他有能力处理int afterQuantity = this.deduct(deductCount);//3. 构建库存变更记录//这里这一步可能会引起争议,如果场因为场景比较多而把大多数转换工作都放到了StockDDDBO上那么可能会导致领域模型变得混乱//比如混淆或者模糊了领域实体本身可以表达的事情,举个例子,我做饭了,花了一些时间,我自己知道,但是记录这件事情的可能是旁边的//女朋友或者说是摄像头,当然这里需要意识到如果是复杂项目就需要考虑是否用mapstruct专门构建转换层了StockRecordBO stockRecordBO = stockDDDBO.buildRecord(StockOperationEnum.DEDUCT, afterQuantity);//4. 持久化库存 和变更记录StockRecordMapper stockRecordMapper = SpringApplicationContext.getBean(StockRecordMapper.class);stockRecordMapper.insert(stockRecordBO.convertToDO());stockMapper.updateQuantity(stockCode,afterQuantity);//注意5,6两步一般来说如果没有其他因素干扰这两步可能需要在事务完成之后走异步事件来进行解耦,//通常来讲扣库存在真实场景下会引起数据库和缓存的不一致问题,这里不过多讨论相关细节,但是总体来说//扣库存的业务操作应该算领域服务里的操作,所以至于怎么具体实现跟一致性需求有关的逻辑还需要进一步构建//业务代码的执行流程。//5. 更新缓存StockCacheService stockCacheService = SpringApplicationContext.getBean(StockCacheService.class);stockCacheService.deduct(stockCode,afterQuantity);//6. 发送mq,这个发送mq的行为可能不会像上面那样会有更多的讨论,一般来说事情执行完之后总需要有消息的,至于// 异步的还是同步的,或者说是等待事务成功的都相对比较好处理,但是我们需要注意的一点是业务BO需要如何把相关上下文// 参数传递给EventStockMqSerivce stockMqSerivce = SpringApplicationContext.getBean(StockMqSerivce.class);stockMqSerivce.sendStockChangeMq(StockChangeEvent.builder().changeQuantity(deductCount).opearationCode(StockOperationEnum.DEDUCT.getCode()).build());return true;}

因为有了SpringApplicationContext这个静态独立类,我们可以很方便的在需要的时候来驱动服务让吸来的内力为自己所用,究其根本原因这么写可能还是因为内力本身就是别人的,所以强行弄到这个方法里不借助SpringApplicationContext这个心法就会让方法行数进一步膨胀,膨胀到无法直视。

4.3 第三版本

那么见识了spring的强大之后还想保留吸功大法的能力,就需要让自己也融入到spring容器里,所以需要接受spring注解的洗礼,这里在第三版本中我们让StockHighBloodV3BO注入到spring容器中作为spring bean对象,那这样的话我们又回到了4.1的版本了,但是我们不再需要关注这些业务对象的初始化和引用了,相当于借助spring容器只是让体内的真气冲撞得到了一定的压制。那结合了之后在4.2中我们将不再需要SpringApplicationContext这个只有半部分的心法能力。
那因为融合了新的武学方法,自然而然也会带来一些新的缺点,所以关于融入的注解这里有两个实现:

@Scope("prototype")
@Scope("request")

这两个实现可以让自己在不同敌人来的时候都可以以一种新的自己来面对,但是之前的自己可能就不知道去哪里了。那么这里可能就是又练就了个影分身之术,分身的生命周期将变得混乱。所以在不同的环境下这个能力可能会出现被反噬的可能。

当然更具体的内容大家可以看一下代码实例。

五、总结

5.1 代码工程说明

整体代码仍然采用springboot工程来进行案例演示,另外也会在工程内置相关DDL和文档,以及在不同场景下的代码注释说明,希望可以引起读者的思考,当然有疑问的话可以通过公众号戳我交流。
工程链接地址如下:https://gitee.com/codergit.com/dddin-action/tree/master/youpinshop/stock-simple-demo

5.2 各种模型和springbean

上面阐述了模型之间的区别,同时我们如果面向领域业务建模编程的话明显不会是失血模型和涨血模型,但是也代表着我们在领域建模和面向业务领域编程的时候我们对各个模型和代码元素的应用将变得更加明确。如果存在涨血模型的话那就需要好好结合spring容器和bean注入相关的知识了,当然如果你有比较好的涨血模型的案例欢迎交流。

各种视角带你做扣库存的逻辑相关推荐

  1. 关于redis做秒杀库存扣减的生产实践及思考

    前言 近期组员接手了一个领券的业务,涉及到了对券批次库存的扣减操作,在多次尝试优化后压测起来仍有一些性能问题,由于接近deadline,于是自己也尝试上手优化了一下.让我对日常在论坛看到的redis秒 ...

  2. 分布式锁和mysql事物扣库存_这个是真的厉害,高并发场景下的订单和库存处理方案,讲的很详细了!...

    前言 之前一直有小伙伴私信我问我高并发场景下的订单和库存处理方案,我最近也是因为加班的原因比较忙,就一直没来得及回复.今天好不容易闲了下来想了想不如写篇文章把这些都列出来的,让大家都能学习到,说一千道 ...

  3. 以程序员的视角带你看西安

    编者荐语: 作为一名土生土长的陕西人,我曾经在西安上了7年的学(本硕),2018年硕士毕业来到深圳,已工作3年左右.最近几年西安的热度不断,抖音上不倒翁小姐姐.大唐不夜城火了,全国十四运这个夏天也要来 ...

  4. 给孩子讲100个科学道理,不如带他做这些趣味实验!

    ▲ 数据汪特别推荐 点击上图进入玩酷屋 玩具和学习看似是两个对立的东西,孩子天性爱玩,家长却希望孩子能多学习. 不一定非要啃课本才能汲取知识,有时候,在轻松有趣的游戏中也能学到课堂上学不到的知识. 让 ...

  5. 春节特惠活动┃给孩子讲100个科学道理,不如带他做这些趣味实验!

    ▲ 数据汪特别推荐 点击上图进入玩酷屋 玩具和学习看似是两个对立的东西,孩子天性爱玩,家长却希望孩子能多学习. 不一定非要啃课本才能汲取知识,有时候,在轻松有趣的游戏中也能学到课堂上学不到的知识. 让 ...

  6. 我和小美的撸码日记(1)之软件也需靠脸吃饭,带您做张明星脸(附后台经典框架 DEMO 下载)...

    众所周知程序员得靠技术吃饭,但是真的光靠技术就够了吗?Teacher苍,一位德艺双馨的艺术家,论技术她自然是炉火纯青,我觉得她桃李遍天下的原因不仅限于些,试想如果Teacher苍长得跟凤姐一样再带点乡 ...

  7. 百度SEO站群Emlog最新付费模板带会员 做资源网不错

    百度SEO站群Emlog最新付费模板带会员 做资源网不错 打开emlog后台导入模板即可使用,拿去做资源网不错~ 下载地址: http://www.bytepan.com/Z4J0oKiDLII

  8. 雷军正式入驻B站,或为小米新品直播带货做准备

    伴随着小米10超大杯的消息愈发火热,雷军也在加紧为其花式预热.昨天,雷军还发布微博称:"有朋友问我,最近流行直播带货和演讲,你怎么看?"疑似为其直播带货做铺垫. 众所周知,目前数码 ...

  9. 基于真实电商的下单扣库存学习理解分布式事务解决方案

    文章目录 业务背景 分布式事务解决方案及缺点 业务过程分析 下单扣减库存的业务难点 解决方案 1. 先扣库存,后创建订单 2. 先创建订单,后扣库存 异常数据处理 1. 库存表流水表 2. 重试+回滚 ...

最新文章

  1. 命令行收集(DOS/Linux/nc/xscan/xsniffer)
  2. 强强联合!Papers with Code 携手 arXiv,上传论文、提交代码一步到位
  3. 09、redis哨兵的多个核心底层原理的深入解析(包含slave选举算法)
  4. 减负提质的新命题下,网易云信如何为课后服务升级?
  5. 10分钟零基础带你入门Ribbon小项目-啥?小白都能看懂?
  6. Bootstrap——table标签使用横向滚动条解决方案
  7. 牛客练习赛 61(待补F-点分治?)
  8. docker 安装 nacos/nacos-server 镜像并配置本地数据库
  9. 385. Mini Parser
  10. C语言 动态开辟内存管理
  11. vue watch 监听不到变化_关于vue中watch检测到不到对象属性的变化的解决方法
  12. 28.TCP/IP 详解卷1 --- SMTP:简单邮件传输协议
  13. Golang让协程交替输出
  14. 操作系统笔记(王道考研) 第一章:计算机系统概述
  15. 【转载】爷爷和我---来自泊小豆的微博
  16. uniapp接收服务器消息,uniapp如何请求服务器数据
  17. 怎么样可以在网络上赚钱,告诉你网上赚钱的5种方法!
  18. ConcurrentHashMap 1.7和1.8 源码解析
  19. C++ SuperLU 混合编程
  20. 工业计算机英语作文,工业计算机,Industrial Computer,音标,读音,翻译,英文例句,英语词典...

热门文章

  1. 你最拿手的5种程序设计语言是什么
  2. range()的使用
  3. fgo服务器维护后抽奖,fgo:抽卡机制详解 彩圈是必定会出五星的
  4. 【Research】Wafer晶圆异常模式检测研究
  5. wps流程图直线上怎么填字_简单三步,用WPS轻松完成一个又大气又好看的流程图!...
  6. 笑话大全api_接口详细介绍-笑话大全
  7. FlexCell控件初始化以及加载数据集[原创]
  8. Thymeleaf模板引擎+Spring整合使用方式的介绍
  9. 黑客利用手机缺陷做出超强微信红包软件!
  10. android 仿ios毛玻璃,类 iOS 毛玻璃效果控件 BlurView