试试给对象这样赋值吧

1,045 阅读8分钟

前言

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

背景

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

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

实际库存=可用库存+采购在途库存+销退在途库存+...+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之类。像上面的库存计算,我们是不是可以计算完一步然后按照链路传递下去,直到没有相关处理的类。
那么我们试试看吧!

实现

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

image.png 我们来看看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 {

    @Autowired
    List<StockCalcBusiness> stockCalcBusinessList;

    @PostConstruct
    public void build() {
        if (CollectionUtils.isEmpty(stockCalcBusinessList)) {
            stockCalcBusinessList = Lists.newArrayList(new StockCalcBusiness() {
                @Override
                public 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 {

    @Override
    public 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 {

    @Autowired
    List<StockCalcBusiness> stockCalcBusinessList;

    public static ExecutorService threadPool = new ThreadPoolExecutor(10, 10,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(10000));

    @PostConstruct
    public void build() {
        if (CollectionUtils.isEmpty(stockCalcBusinessList)) {
            stockCalcBusinessList = Lists.newArrayList(new StockCalcBusiness() {
                @Override
                public 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 {

    @Override
    public 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,我觉得你这样的状态面试很危险哟!)。
当然如果是强调顺序的流程,例如工艺品的只做流程之类的项目开发,则还是老老实实使用真正的责任链开发模式吧。

最后

如果我的文章对你有所帮助,还希望各位大佬点个关注\color{red}{点个关注} 点个赞\color{red}{点个赞},再次感谢大家的支持!
这里也附上我的Github地址:github.com/showyool/ju…

欢迎大家关注我的微信公众号:【一段有温度的代码】