一、背景
1.1 背景说明
之前群里有人分享基于贫血模型和充血模型相关的一些代码实战,同时也有一些小伙伴不太理解这些模型之间的真正内容,本文将通过一个扣库存的代码工程实践来阐述不同视角下的扣库存逻辑的实现,当然在阅读本文之前可以看一下各种模型的相关具体介绍:https://www.cnblogs.com/yinhaiming/articles/1564843.html
当然文章已经比较早了,就目前而言面向spring编程将让我们更少的关注这些模型之间的差异,所以近年来对这些模型的讨论热度已经慢慢降下来了。
1.2 扣库存需求说明
这里对扣库存的逻辑和需求做进一步的介绍,避免出现理解不一致,另外也了体现一定的业务复杂度,需求如下:
-
通过controller层发送扣库存请求
-
在service中处理扣库存逻辑,判断库存记录是否存在,存在则执行扣库存逻辑,不存在返回扣库存失败
-
扣成功之后需要做以下三件事情:
-
记录库存变更日志
-
更新库存缓存
-
发送库存变更记录
-
整体扣库存逻辑完成之后需要返回是否扣成功
二、常规视角(贫血模型)
2.1 模型介绍
我们先看一下常规视角下的业务模型,这里的业务对象有两个,如下:
2.2 service接口说明
2.3 常规视角下的service实现
@Override
public 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--> StockRecoredDO
StockRecordDO 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. 发送mq
stockMqSerivce.sendStockChangeMq(StockChangeEvent.builder()
.changeQuantity(deductCount)
.opearationCode(StockOperationEnum.DEDUCT.getCode()).build());
return true;
}
三、领域建模视角(充血模型)
3.1 失血模型/贫血模型/充血模型的略微区别
-
失血模型
domain object只有属性的getter/setter方法的纯数据类。这里需要注意的是上面文章中的domain object也可以直接被持久化层引用,相当于domain object有两个含义,一个是算业务对象一个算持久层数据模型,那在引入了分层架构等因素之后,我们现在讲的是业务对象,所以这里需要有点区别说明。
-
贫血模型
domain ojbect包含了不依赖于持久化的领域逻辑。由于我们把一些不依赖外部服务实现的逻辑放到了domain object中,所以这里就有点贫血的意思,但是并不是说所有不依赖持久化的逻辑都放到domain object也算贫血模型,因为这样的话可能算做充血模型了。在现在的复杂技术体系下关于缓存和mq还有数据转换等操作都将不太算做持久化的内容。
-
充血模型
充血模型和第二种模型差不多,所不同的就是如何划分业务逻辑。这里的划分逻辑在文中是要表达让service如同controller层一样只是一层壳而已,那么大多数业务逻辑将被domain object持有。所以在一般实践上我们不会太纠结于是贫血模型还是充血模型。因为模型一旦有了更丰富的行为那终将不太好分辨充血模型,或者大多数情况下如果实现了充血模型的话,我们的业务代码可能会有两层壳,不太符合复杂业务的开发习惯。
所以这里总结一下,如果domain object跟数据库实体分离的话,那么数据库实体一般就是失血模型了,当如果domain object也是失血模型,的话对于service来说可能就是过程式的面条型业务逻辑。所以比较好的是在分离的情况下让dto和数据库实体作为失血模型或者贫血模型存在,让domain object作为弱充血模型存在。那么在整个复杂service层下我们会有更多的可操作性来更快的迭代和重构。同时我们不需要纠结于概念是怎么样的,去执着于概念的标准实现。
3.2 领域模型下的StockBO
从上面的介绍来看,贫血模型和充血模型的界限不是特别明确,当然就跟喝酒一样,喝少了脸上不咋红,喝晕了就红扑扑的了,喝醉了可能要送医了。那现在我们看一下领域建模视角下的StockBO对象内容:
3.3 领域服务视角下的service实现
@Override
public 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需要如何把相关上下文
// 参数传递给Event
stockMqSerivce.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需要如何把相关上下文
// 参数传递给Event
StockMqSerivce 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注入相关的知识了,当然如果你有比较好的涨血模型的案例欢迎交流。
原文始发于微信公众号(神帅的架构实战):各种视角带你做扣库存的逻辑
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/241573.html