1. 业务场景描述

1.1 库存中心的业务操作

  • 增加:通过采购入库,增加库存
  • 锁定:下单之后锁定库存
  • 解锁:订单取消之后,释放库存
  • 扣减:订单支付成功之后扣减库存,扣减锁定库存
  • 返还:退货之后返还库存,增加库存

1.2 库存中心三种商品库存数量

  • 可销售库存数量 SaleStockQuantity,通常显示在商品详情页,用户提交订单后,可销售库存的商品数量-1
  • 锁定库存数量 LockedStockQuantity,用户提交订单后,锁定库存中的商品数量+1,当用户支付订单后锁定库存中的商品数量-1
  • 已销售库存数量 SaledStockQuantity,当用户支付订单之后,已销售库存数量+1

1.3 系统中需要调用库存接口的场景大概有

  • 网站上的商品详情页中显示的库存信息(查询可销售库存数量)
  • 采购入库之后,采购中心会调用库存中心接口,通知库存中心将某个商品的 SaleStockQuantity 累加上采购数量 PurchaseInputQuantity
  • 提交订单后,订单中心调用库存中心接口,通知库存中心将商品的 SaleStockQuantity-1,同时 LockedStockQuantity+1
  • 支付订单后,订单中心会调用库存中心接口,通知库存中心将商品的 LockedStockQuantity-1,同时将商品的 SaledStockQuantity+1
  • 取消订单后,订单中心会调用库存中心接口,通知库存中心将商品的 LockedStockQuantity-1,同时将商品的 SaleStockQuantity+1
  • 退货入库后,仓库中心会调用存库中心接口,通知库存中心将商品的 SaleStockQuantity+1,同时将商品的 SaledStockQuantity-1

2. 不使用设计模式来实现

以采购入库、提交订单为例,用伪代码来实现这两个业务流程:

  • InventoryFacadeServiceImpl 实现类 ```java public class InventoryFacadeServiceImpl implements InventoryFacadeService{

    /**

    • @Description 通知库存中心,“采购入库完成”事件发生了 **/ public Boolean informPurchaseInputFinished(PurchaseInputOrder 采购入库单){ // 1. 从采购入库单,获取采购商品sku id集合 // a. 从采购入库单获取采购条目 List PurchaseInputOrder.getItems() // b. 遍历list,goodsSkuIds.add(item.getGoodsSkuId()) // c. 返回 List List getGoodsSkuIds(采购入库单);

      // 2. 通过商品sku id集合,创建商品库存GoodsStock集合 // a. 遍历goodsSkuIds集合,根据sku id获取商品在库存中心的库存信息 // b. 如果没有库存信息,创建这个商品的空数据库存信息,插入数据库,且添加到GoodsStock集合 // c. 如果有库存信息,添加到GoodsStock集合 // d. 返回 List List createGoodsStocks(List goodsSkuIds);

      // 3. 准备一个采购入库单条目 Map 集合 Map itemMap // a. 从采购入库单获取采购条目集合 List // b. 遍历采购条目集合,itemMap.put(item.getSkuId(), item) Map itemMap = getItemList(采购入库单);

  1. // 开始更新库存,遍历商品库存 GoodsStock 集合
  2. for(GoodsStock goodsStock : GoodsStocks){
  3. // 4. 更新商品的可销售库存数量
  4. // a. 从itemMap获取采购入库单条目 item= itemMap.get(goodsStock.getSkuId())
  5. // b. 将条目中的采购数量累加到商品的可销售库存
  6. // goodsStock.setSaleStockQuantity(采购数量+原可销售库存);
  7. updateSaleStockQuantity();
  8. // 5. 更新商品的库存状态
  9. // a. 如果可销售库存大于0,则状态:goodsStock.setStockStatus(有库存)
  10. // b. 否则,goodsStock.setStockStatus(无库存)
  11. updateStockStatus();
  12. // 6. 更新商品库存的修改事件
  13. // a. goodsStock.setGmtModified(当前时间);
  14. updateGmtModified();
  15. // 7. 执行数据库更细商品库存的操作
  16. // a. goodsStockDAO.updateGoodsStock(goodsStock);
  17. executeUpdateGoodsStock();
  18. }

}

/**
  • @Description 通知库存中心,“提交订单”事件发生了 **/ public Boolean informSubmitOrderEvent(订单信息){ // 1. 从订单,获取商品sku id集合 // a. 从订单获取商品条目 List order.getItems() // b. 遍历list,goodsSkuIds.add(item.getGoodsSkuId()) // c. 返回 List List getGoodsSkuIds(采购入库单);

    // 2. 通过商品sku id集合,创建商品库存GoodsStock集合 // a. 遍历goodsSkuIds集合,根据sku id获取商品在库存中心的库存信息 // b. 如果没有库存信息,创建这个商品的空数据库存信息,插入数据库,且添加到GoodsStock集合 // c. 如果有库存信息,添加到GoodsStock集合 // d. 返回 List List createGoodsStocks(List goodsSkuIds);

    // 3. 准备一个订单商品条目 Map 集合 Map itemMap // a. 从订单获取商品条目集合 List // b. 遍历订单商品条目集合,itemMap.put(item.getSkuId(), item) Map itemMap = getItemList(订单);

// 开始更新库存,遍历商品库存 GoodsStock 集合
for(GoodsStock goodsStock : GoodsStocks){

  // 4. 更新商品的可销售库存数量
  //   a. 从itemMap获取订单商品条目 item= itemMap.get(goodsStock.getSkuId())
  //   b. 将商品的可销售库存减去商品条目中购买的数量 
  //      goodsStock.setSaleStockQuantity(原可销售库存-购买数量);
  updateSaleStockQuantity();

  // 5. 更新所锁定库存数量
  //   a. 从itemMap获取订单商品条目 item= itemMap.get(goodsStock.getSkuId())
  //   b. 将商品的锁定库存累加商品条目中购买的数量
  //      goodsStock.setLockedStockQuantity(原锁定库存+购买数量);
  updateLockedStockQuantity();

  // 6. 更新商品的库存状态
  //   a. 如果可销售库存大于0,则状态:goodsStock.setStockStatus(有库存)
  //   b. 否则,goodsStock.setStockStatus(无库存)
  updateStockStatus();

  // 7. 更新商品库存的修改事件
  //   a. goodsStock.setGmtModified(当前时间);
  updateGmtModified();

  // 8. 执行数据库更细商品库存的操作
  //   a. goodsStockDAO.updateGoodsStock(goodsStock);
  executeUpdateGoodsStock();

} }


<a name="KpHDY"></a>
## 3. 使用设计模式
<a name="ShcOM"></a>
### 3.1 模板方法设计模式
**模式核心思想:**<br />如果一个场景下,涉及到多个不同的处理逻辑,但是它们又有共同的基础逻辑,那么我们可以将共同的基础逻辑抽取到父类中,然后将需要子类去实现的逻辑留空,交给子类去实现。

像上面的更新库存的业务流程,其实是有一个固定的流程来更新库存,例如:先更新可销售库存、后更新锁定库存、最后更新销售库存、接着检查库存状态、设置修改时间、最后执行更新;

**在不使用设计模式的情况:**<br />可能每个接口由不同的人实现,可能逻辑都是对的,但是每个具体的流程可能执行顺序就不一样,导致不同的更新逻辑似乎差别很不一样,对后期的代码的维护和理解增加难度;

**在使用模板方法设计模式的情况下:**

- 首先,将一些通用的逻辑抽取到父类中;
- 其次,可以在父类定义模板方法,模板方法定义一些子方法,留给子类去实现。这样基于模板的模式,就可以限定每个库存更新的过程都是按照一样的步骤去执行,很清晰;
- 最后,如果后续要修改库存更新的逻辑,或者新增一种库存更新的逻辑,都是按照这个模板顺序去执行,很容易读懂,逻辑不会出错;

<a name="TbAKd"></a>
### 3.2 命令模式
**模式核心思想:**<br />一些场景下,可能会执行一些不同逻辑的操作,这些操作都实现了一个接口,但是将不同类型的逻辑操作封装成不同的命令对象;

例如,库存更新场景下,将不同类型的库存更新操作,封装成不同的库存更新命令,这里暂时只是简单的执行这个命令中的更新逻辑,但是后续可以对库存更新操作扩展一下,例如实现异步化,库存更新操作的撤销这些操作;

<a name="nQHKO"></a>
### 3.3 工厂方法设计模式
**模式核心思想:**<br />工厂模式的目的就是为了不在自己的代码里面手动 new 一个实现类出来,真正做到面向接口编程,调用方不用 care 具体是哪一个实现类;

这样的话,如果 new 出来的实现类,如果有一天需要换一个新的实现类逻辑,调用方就不用关注这些实现类的变动,由工厂类来做出修改,提供实现类的实例,调用方面向接口编程,接口不变,调用方代码就不用变;

库存更新的逻辑这里,可以实践一下工厂模式,不同的库存更新命令,需要不同的工厂来创建这些命令,但是这些工厂之间有一些通用逻辑,可以抽取到父工厂里面,采用模仿方法模式+工厂模式,子工厂可以专注干自己的事情就可以了;

<a name="s8Axd"></a>
### 3.4 具体实现
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1471554/1637315934776-6d95ed04-cdeb-4270-a00e-32a4ca56a508.png#clientId=ue026d90a-18b0-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=298&id=u267467fe&margin=%5Bobject%20Object%5D&name=image.png&originHeight=596&originWidth=1419&originalType=binary&ratio=1&rotation=0&showTitle=false&size=140386&status=done&style=none&taskId=u15810dc5-b051-4d34-baf2-fd1617a4cf8&title=&width=709.5)
<a name="QGJfc"></a>
#### (1) 工厂接口
```java
// 面向接口编程
public interface StockUpdaterFactory<T>{
    StockUpdateCommand create(T parameter); // T parameter 可以是采购入库单、订单
}

(2) 抽象工厂父类

// 模板方法模式在父类定义一个模板方法,完成基础通用逻辑,留空特殊逻辑给子类实现
public abstract class AbstractStockUpdaterFactory<T> implements StockUpdaterFactory<T> {

    // 实现接口定义的方法,作为模板方法
    @Override
    public StockUpdateCommand create(T parameter) {
        // 1. 从订单,获取商品sku id集合
        //   a. 从订单获取商品条目 List<OrderItem> order.getItems()
        //   b. 遍历list,goodsSkuIds.add(item.getGoodsSkuId())
        //   c. 返回 List<sku id>
        List<Long> getGoodsSkuIds(采购入库单、订单); // 通用逻辑,父类自己实现

        // 2. 通过商品sku id集合,创建商品库存GoodsStock集合
        //   a. 遍历goodsSkuIds集合,根据sku id获取商品在库存中心的库存信息
        //   b. 如果没有库存信息,创建这个商品的空数据库存信息,插入数据库,且添加到GoodsStock集合
        //   c. 如果有库存信息,添加到GoodsStock集合
        //   d. 返回 List<GoodsStock>
        List<GoodsStock> createGoodsStocks(List<Long> goodsSkuIds); // 通用逻辑,父类自己实现

        //返回命令类,不同的子工厂返回不同的命令,留待子类工厂去实现
        return createStockUpdateCommand(goodsSkuIds, parameter); 
    }

    // 留空,待子类工厂去完善创建不同的命令对象
    protected abstract StockUpdateCommand createStockUpdateCommand(
            List<GoodsStockDO> goodsStockDOS, T parameter)
}

(3) 采购入库更新库存命令的工厂类

// 子工厂类,返回采购入库更新命令,完成父类中留空的逻辑
@Component
public class PurchaseInputStockUpdaterFactory<T> extends AbstractStockUpdaterFactory<T>{

    // 完善父类留空的创建命令的方法,具体实现采购入库更新的命令的创建
    protected abstract StockUpdateCommand createStockUpdateCommand(
            List<GoodsStockDO> goodsStockDOS, T parameter){

        // 3. 准备一个采购入库单条目 Map 集合 Map<sku id, PurchaseInputOrderItem> itemMap
        //   a. 从采购入库单获取采购条目集合 List<PurchaseInputOrderItem>
        //   b. 遍历采购条目集合,itemMap.put(item.getSkuId(), item)
        Map<sku id, PurchaseInputOrderItem> itemMap = getItemList(采购入库单);

        // 返回采购入库更新库存命令
        StockUpdateCommand command = new PurchaseInputStockUpdateCommand(
            goodsStockDOS, itemMap)
    }
}

(4) 更新库存命令接口

// 面向接口编程
public interface StockUpdateCommand {
    // 更新库存操作
    Boolean updateGoodsStock();
}

(5) 更新库存命令的抽象父类

// 根据模板方法模式,在父类定义模板方法,执行一些固定步骤的流程,具体流程留空待子类实现
public abstract class AbstractStockUpdateCommand implements StockUpdateCommand{
    // 构造函数
    public AbstractStockUpdateCommand(List<GoodsStockDO> goodsStockDOS) {
        this.goodsStockDOS = goodsStockDOS;
    }

    // 实现接口定义的方法,作为模板方法
    // 更新库存的固定流程,可留空,待不同命令去具体实现
    @Override
    public Boolean updateGoodsStock(){
        // 步骤1~5对库存信息对象的关键字段进行更新操作
        // 步骤1. 更新可销售库存
        updateSaleStockQuantity();
        // 步骤2. 更新锁定库存
        updateLockedStockQuantity();
        // 步骤3. 更新已销售库存
        updateSaledStockQuantity();
        // 步骤4. 更新库存状态
        updateStockStatus();
        // 步骤5. 更新库存状态
        updateGmtModified();
        // 步骤6. 将更新后的库存信息对象,更新到数据库
        executeUpdateGoodsStock();
    }

    // 以下方法都是通用逻辑,放在父类    
    private void executeUpdateGoodsStock(){
        // 对库存信息对象进行持久化
    }
    private void updateGmtModified(){
        // 更新库存的修改时间字段
    }
    private void updateStockStatus(){
        // 更新库存的状态字段
    }

    // 以下方法留空,待具体的命令子类去决定实现哪些流程
    protected abstract void updateSaledStockQuantity();
    protected abstract void updateLockedStockQuantity();
    protected abstract void updateSaleStockQuantity();

}

(6) 采购入库更新命令

// 模板方法的具体实现类
public class PurchaseInputStockUpdateCommand extends AbstractStockUpdateCommand {

    // 1. 准备一个订单商品条目 Map 集合 Map<sku id, OrderItem> itemMap
    Map<sku id, PurchaseInputOrderItem> itemMap;

    // 2. 构造函数
    public PurchaseInputStockUpdateCommand(
            List<GoodsStockDO> goodsStockDOS,
            Map<Long, PurchaseInputOrderItemDTO> purchaseInputOrderItemDTOMap) {
        super(goodsStockDOS);
        this.purchaseInputOrderItemDTOMap = purchaseInputOrderItemDTOMap;
    }

    @Override
    protected void updateSaleStockQuantity() throws Exception {
        // 更新sku id, 对应的商品库存信息中的可销售库存
    }

    @Override
    protected void updateSaledStockQuantity() throws Exception {
        // 采购入库,不需要更新已销售库存,留空
    }
    @Override
    protected void updateLockedStockQuantity() throws Exception {
        // 采购入库,不需要更新锁定库存,留空
    }
}

(7) Service 中调用各种库存更新命令

@Service
public class InventoryFacadeServiceImpl implements InventoryFacadeService {
    // 注入采购入库命令的工厂
    @Autowired
    private PurchaseInputStockUpdaterFactory<PurchaseInputOrderDTO>
            purchaseInputStockUpdaterFactory;


    /**
     * @Description 通知库存中心,“采购入库完成”事件发生了
     * @Param [purchaseInputOrderDTO] 采购入库单DTO
     * @return Boolean
     **/
    @Override
    public Boolean informPurchaseInputFinished(
            PurchaseInputOrderDTO purchaseInputOrderDTO) {
        try {
            StockUpdateCommand goodsStockUpdateCommand =
                    purchaseInputStockUpdaterFactory.create(purchaseInputOrderDTO);
            goodsStockUpdateCommand.updateGoodsStock();
        }catch (Exception e){
            LOG.error("error", e);
            return false;
        }
        return true;
    }
}