前言

最近在接手库存相关的业务。由于金三银四跳槽季的到来,公司的一些小伙伴终于还是选择了离开。于是交接和开发便成了这一阵子的主要工作内容(啥,你问我为啥不跳槽,还不是因为菜没人要T_T)。
看了几天代码,在熟悉业务的同时,也发现了这些模块开发当中一些值得诟病的地方,所以我觉得可以拿出来可以分享一下,也当做是一种自省。

背景

库存服务这个系统主要是对接各个电商平台当中库存上传功能,即将自有系统当中的库存按照一定的策略上传到平台当中。由于不同的平台会采用不同的策略,所以导致系统库存对应到各个平台上则会显示不同的库存数量,这里就会涉及到一个库存计算的过程,并且这个策略是存在动态变化,也就导致计算也是实时计算。

 上图则是一个简单的流程:当接收到交易行为或者采购行为等影响库存变化的事件的时候,这个时候就要获取这个商品关联的平台并且是开启了上传功能的,接着就是计算库存并且把最终结算结果上传至平台。
本质上这是一个简单的流程(当然有些步骤我就省略了,我这里只是摄取最简化的流程),也不是太难理解。但是当我看到这个库存计算的过程的时候,觉得这样的写法显得十分鸡肋,为此想着可以优化一下。
可能对于非电商业务背景的同学来说,库存这个概念可能不是那么深刻。大家所能看到的库存就是淘宝京东上选择商品之后,显示还有多少商品的数量。但是对于电商商家来说,这个库存可能是有多个部分组成的:

实际库存=可用库存+采购在途库存+销退在途库存+...+XX库存

这里为了示例,所以我们不防定义如下库存数据结构

public class Stock {/*** itemId是款ID* skuId是skuId* 两个ID确定最细粒度的商品*/private Long itemId;private Long skuId;private Long availableStock;private Long purchaseStock;private Long returnStock;/*** 最终展示的库存*/private Long totalShowStock;// 省略get、set方法public Long getTotalShowStock() {return this.availableStock + this.purchaseStock + this.returnStock;}}
复制代码

分析

那么在描述问题之前,我想请大家这样思考下:如果是你,你会怎么进行库存的计算呢?
这个问题是不是显得有点愚蠢,大家是否会觉得,这个不是一个很普通的场景吗,甚至都不觉得会有问题,可能的写法如下:

public Long stockCalc(Stock stock) {Long finalStock = 0L;finalStock += stockCalcWithAvailableStock(stock);finalStock += stockCalcWithPurchaseStock(stock);finalStock += stockCalcWithReturnStock(stock);.........return finalStock;
}private Long stockCalcWithAvailableStock(Stock stock) {// 获取可用库存逻辑return 0L;
}private Long stockCalcWithPurchaseStock(Stock stock) {// 获取采购在途库存逻辑return 0L;
}private Long stockCalcWithReturnStock(Stock stock) {// 获取销退在途库存逻辑return 0L;
}
复制代码

大致上是这样的写法,没有什么毛病,因为原本的库存服务也是这样写的。
但是各位有没有发现,这个stockCalc方法里面会充斥着大量的库存计算。因为这些部分的库存信息并不是直接存在表的一条记录当中,就不得不从其他地方查询(可能是RPC,可能去其他库表等)。这里虽然抽成各个方法,在形式上追求到了代码之美,但是还是显得十分臃肿。这里我只展示了库存当中3个组成部分的计算,但是实际当中可能存在十几个组成部分。
那么如果这个时候,又有一个新的库存概念,比如可配调整库存,那么我在stockCalc方法当中就又要去写一个stockCalcWithAdjustStock方法,这样就明显破坏代码的开发设计规范。

拓展

由于我当前遇到的工作中是库存计算出现这个场景,其实我们日常的开发当中会有很多类似的地方,例如某一个用户的配置信息,我这里就用UserConfig来表示。我们为了去填充UserConfig这个信息,就会像上面一样,一步一步地去获取信息,然后进行赋值,于是就会出现一个很臃肿的方法。如果这个用户信息又有新的属性,那么我们就又会在这个方法里面继续写代码,和上面丰富库存信息的场景其实是一样的。

思路

那么有什么办法呢?可以观察到,库存的计算,其实是按顺序一步一步执行下来,所以我第一反应想到的是责任链开发模式。
说起责任链开发模式,我想大家能想到的是strust2当中用到的,还有SpringMVC当中的拦截器,还有Tomcat当中FilterChain之类。像上面的库存计算,我们是不是可以计算完一步然后按照链路传递下去,直到没有相关处理的类。
那么我们试试看吧!

实现

既然确定了责任链开发模式,那么我们还是先画一个类图来清晰一下:

 我们来看看StockCalcBusiness和StockCalcManager是怎么实现的:

public abstract class StockCalcBusiness {private StockCalcBusiness next;public final void handle(Stock stock) {// 先处理当前环境下的库存this.stockCalc(stock);// 再处理下一个责任链当中的库存if (this.next != null) {this.next.handle(stock);}}public void setNext(StockCalcBusiness stockCalcBusiness) {this.next = stockCalcBusiness;}public abstract void stockCalc(Stock stock);}复制代码
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import javax.annotation.PostConstruct;
import java.util.List;@Component
@Slf4j
public class StockCalcManager {@AutowiredList<StockCalcBusiness> stockCalcBusinessList;@PostConstructpublic void build() {if (CollectionUtils.isEmpty(stockCalcBusinessList)) {stockCalcBusinessList = Lists.newArrayList(new StockCalcBusiness() {@Overridepublic void stockCalc(Stock stock) {log.info("nothing to deal");}});} else {for (int i = 0; i < stockCalcBusinessList.size() - 1; i++) {stockCalcBusinessList.get(i).setNext(stockCalcBusinessList.get(i + 1));}}}public void stockCalc(Stock stock) {this.stockCalcBusinessList.get(0).handle(stock);log.info("计算结束");}}复制代码

当然,这里我们默认是集成在Spring容器当中。由于@PostConstruct的影响,所以当Spring Bean初始化之后就会去执行这里声明的build方法。在初始化过程当中,会将容器当中的StockCalcBusiness的类(由于StockCalcBusiness是抽象类,这里的类指的是继承的子类)注入到这个集合当中。而这里的build方法,其实就是在将这写StockCalcBusiness的子类形成一条链路。
于是在调用stockCalc的时候,会顺着第一个StockCalcBusiness的子类依次掉用handle方法,而handle方法在计算当前库存的之后,又会顺着链路继续执行下去。
细心的小伙伴肯定发现了,这里我还使用到了模板设计模式,也就是让各个子类实现StockCalcBusiness#stockCalc方法,这个地方就可以细细品味。
那这样做的好处是什么呢?按照我们上面说的,如果我又要实现可配调整库存,那么按照原本的做法,会是那个方法变得越来越长,越来越臃肿,而这里,我们只需要实现StockCalcBusiness的一个子类即可。

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;@Component
@Slf4j
@Order(4)
public class AdjustStockCalcBusiness extends StockCalcBusiness {@Overridepublic void stockCalc(Stock stock) {log.info("计算可配调整库存");if (StockType.checkAdjustStock(stock.getBit4Stock())) {// 这里就省略调整库存的获取过程stock.setAdjustStock(20L);} else {stock.setAdjustStock(0L);}}}复制代码

由于Spring的自动注入机制,会把这个AdjustStockCalcBusiness加入到这个链式当中。这样我们只需关心这个类当中的这部分业务计算即可,就不会造成原先业务方法变得很臃肿的问题。

后续

在我们这样优化这个库存计算之后,果然,后续就有产品提需求,需要将另一个库存领域概念加入到库存的计算当中,于是我就再也不用在之前臃肿的方法当中去继续添加代码。

思考

当然,其实这种开发模式固然是不错的,却也有一定的瓶颈:

  1. 如果子类很多,那么这一个链路会比较深,严重的情况下可能存在栈溢出的情况。
  2. 有些情况下,我们对数据的获取希望是并发模式,而不一定是追求同步。

对于第一种考量,其实是要对业务的分析以及预估。我们可以遇到,这种链式可能在一个可控的范围之内,那么就可以是这种做法。
对于第二种考量,其实我个人是保留意见的。首先,既然是责任链开发模式,那么其实从字面意思上已经表明,这里的处理是按照链路的方式线性执行,也就是所谓的同步执行。但是很多情况下,我们希望说是能够并发执行,并发的好处我想我就不用多说了,其实最主要的还是为了提高响应速度,提高效率。
那么我们也可以进行简单处理下,这里我们会用到JUC下面的CountDownLatch。我们将这个CountDownLatch加入到Stock对象当中,将StockCalcManager简单改造一下:

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import javax.annotation.PostConstruct;
import java.util.List;
import java.util.concurrent.*;@Component
@Slf4j
public class StockCalcManager {@AutowiredList<StockCalcBusiness> stockCalcBusinessList;public static ExecutorService threadPool = new ThreadPoolExecutor(10, 10,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(10000));@PostConstructpublic void build() {if (CollectionUtils.isEmpty(stockCalcBusinessList)) {stockCalcBusinessList = Lists.newArrayList(new StockCalcBusiness() {@Overridepublic void stockCalc(Stock stock) {log.info("nothing to deal");}});} else {for (int i = 0; i < stockCalcBusinessList.size() - 1; i++) {stockCalcBusinessList.get(i).setNext(stockCalcBusinessList.get(i + 1));}}}public void stockCalc(Stock stock) {stock.setCountDownLatch(new CountDownLatch(stockCalcBusinessList.size()));this.stockCalcBusinessList.get(0).handle(stock);try {stock.getCountDownLatch().await();} catch (InterruptedException e) {e.printStackTrace();}log.info("计算结束");}}
复制代码

并且,我们在StockCalcBusiness的子类也简单改造一下,例如AdjustStockCalcBusiness:

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import java.util.Random;@Component
@Slf4j
@Order(4)public class AdjustStockCalcBusiness extends StockCalcBusiness {@Overridepublic void stockCalc(Stock stock) {StockCalcManager.threadPool.submit(() -> {log.info("计算调整库存");if (StockType.checkAdjustStock(stock.getBit4Stock())) {// 这里就省略调整库存的获取过程stock.setAdjustStock(20L);} else {stock.setAdjustStock(0L);}try {Thread.sleep(1000 * new Random().nextInt(10));} catch (InterruptedException e) {e.printStackTrace();}stock.getCountDownLatch().countDown();});}}复制代码

这里我们用线程池和CountDownLatch的组合进行并发的控制,也就是说,我们在传递链路的时候,只是把任务进行了提交,直到最后一个节点提交任务。这个链路看上去是完成了,但是由于CountDownLatch的存在,所以主线程需要等待所有持有CountDownLatch的处理类完成之后,将AQS当中的state值countDown至0,这才算是真正结束(什么?AQS的state值?你在说什么?OK,我觉得你这样的状态面试很危险哟!)。
当然如果是强调顺序的流程,例如工艺品的只做流程之类的项目开发,则还是老老实实使用真正的责任链开发模式吧。

本文首发于java黑洞网,csdn同步更新

试试给对象这样赋值吧相关推荐

  1. java hashmap 初始化赋值_HashMap引用传递,对象直接赋值,修改的是同一个对象,new HashMap「」(Map)才是生成新的对象...

    HashMap引用传递:对象直接赋值,修改的是同一个对象 MapsrcMap = new HashMap<>(); srcMap.put("a","a&quo ...

  2. 利用dynamic解决匿名对象不能赋值的问题

    关于匿名对象 匿名对象是.Net Framework 3.0提供的新类型,例如: var obj = { Name = "aa", Value = 1 }; 就是一个匿名类,搭配L ...

  3. 对象的赋值和复制(转)

    一.对象的赋值和复制 1.对象的赋值 如果对一个类定义了两个或多个对象,则这些同类的对象之间可以互相赋值,或者说,一个对象的值可以赋给另一个同类的对象.这里所指的对象的值是指对象中所有数据成员的值. ...

  4. java类向拦截器传值_MyBatis拦截器:给参数对象属性赋值的实例

    该拦截器的作用:在进行增加.修改等操作时,给数据模型的一些通用操作属性(如:创建人.创建时间.修改人.修改时间等)自动赋值. 该实现是在dao层拦截,即存入db前最后一层.后经分析,不是很合理,改为在 ...

  5. 《Python Cookbook 3rd》笔记(1.2):拆分任意长可迭代对象后赋值给多个变量

    拆分任意长可迭代对象后赋值给多个变量 问题 若一个可迭代对象的元素个数超过变量个数时,会抛出一个 ValueError,那如何才能从这个可迭代对象中拆分出N个元素出来? 解法 Python的星号表达式 ...

  6. C++对象的赋值和复制

    C++对象的赋值 1.1对象之间的赋值是用"="运算符来实现的,"="在c++中扩展为重载运算符来实现对象间的赋值. t1=t2; 1.2对象赋值是对数据成员的 ...

  7. java两个对象赋值_一起学Java(二十六)----- 对象之间赋值

    不积跬步,无以至千里:不积小流,无以成江海. Java语言基础 Java对象之间赋值 赋值是用等号运算符" = "进行的,在对对象进行"赋值"时,实际就是将句柄 ...

  8. 通过反射为对象属性赋值

    /// <summary>/// 通过反射为对象属性赋值/// </summary>/// <typeparam name="T">类型参数&l ...

  9. C++中对象的赋值拷贝构造函数

    目录 1.对象与对象之间的赋值. 下面给出代码说明赋值语句 对象赋值的限制和特点 2.拷贝构造函数 拷贝构造函数的特点 自定义的拷贝构造函数的代码及运行结果 默认拷贝构造函数 调用拷贝构造函数的3种情 ...

最新文章

  1. faster rcnn在自己的数据集上训练
  2. JavaScript获取URL参数
  3. for循环如果先--_乐字节Java循环:循环控制和嵌套循环
  4. 1.2w 星!火爆 GitHub 的 Python 学习 100 天
  5. 测试人员眼中的问题解决策略
  6. 转化率模型之转化数据延迟
  7. 不同表_一个公式搞定数据信息按类别拆分到不同工作表
  8. 常见基本题型:进制的转换
  9. photoshop是什么软件,它能做什么?
  10. 5.2为每种类型的模块内聚举一个例子
  11. 分布式强化学习方法汇总
  12. uniapp实现app跳转app
  13. 怀仁一中2021高考成绩查询,2019怀仁一中录取分数线(附2019高考成绩喜报)
  14. 微信公众号开发——模板消息
  15. 小程序开发需要学什么语言呢?看看吧
  16. Android 7.1 车机 Android 系统 在线升级,将安装到下载到/data/目录下面
  17. 涂子沛:个人信息保护入法,堵住非法变现通道
  18. MySQL 删除数据 批量删除(大量)数据
  19. 惠普中国CEO孙振耀退休感言【转】
  20. 2008 Jolt 大奖新鲜出炉_华章公司国内首家报道

热门文章

  1. soap和web services
  2. 帮MM修电脑的三个步骤-此文绝对实用
  3. sparksql优化_Spark SQL amp; Streaming
  4. Spark源码分析之Sort-Based Shuffle读写流程
  5. (216)滤波器介绍
  6. (99)FPGA最大延迟与最小延迟基础
  7. Vivado过程文件解释
  8. Python入门级2
  9. 八位流水灯的verilog代码_Arduino入门 第七节-彗星灯 呼吸流水灯
  10. 数字图像处理(四)——图像编码技术(二)