重构视角,如何写“好代码“——组织方法篇

208 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

如何写”好代码“?这是一个困扰很多开发人员的问题。当然,写”好代码“并不是一件容易的事。优雅的算法、合适的数据结构、规范的代码、面向对象设计思想、合理的设计模式、系统设计等方面的内容,都影响着写出好代码。现在,我们尝试用另外一个角度来学习学好代码:重构。所谓重构,就是改善既有代码的设计。重构既然可以改善代码的既有设计,那么为什么不在开始写代码的时候,就用重构的某些方式直接把代码写好、写完善?今天,这篇文章就以重构视角聊聊怎么写出好的方法(Method)。

提炼方法(Extract Method)

我们可能不能分辨什么是好的代码,但是一定能知道什么是不好的代码(代码不好的地方,常被我们称作代码的”坏味道“)。对于方法(Method)层面的”坏味道“,有一个显而易见的问题就是:方法过长。想想,如果让你接手一个有成百上千行代码的方法,你一定会”疯掉“。长方法有很多弊端:很难被复用;代码可读性差;难以被覆写。

对于方法过长的这种情况,重构最简单的方式,就是提炼方法,将方法部分代码放进一个独立方法中,并让方法名称解释该方法的用途。而我们在开始写代码的时候,就要注意方法的提炼!

什么时候应该提炼出新的方法呢?

  • 方法过长时
  • 需要注释才能让人理解用途的代码
  • 语义层次不同时。语义层次是什么意思呢,比如,我们完成某个业务时,需要步骤1、2、3,而步骤1又可以拆分成步骤1.1、1.2、1.3。这样步骤1、2、3与步骤1.1、1.2、1.3 语义层次就不同了。

示例:

业务场景:添加商品到购物车。业务逻辑很简单,校验商品是否可加入购物车,组装购物车实体,更新到数据库,

原代码:

/**
 * 添加购物车
 * @param userSn
 * @param req
 */
public void add(Long userSn, CartAddReq req) {
    Long skuId = req.getSkuId();
    // 获取sku商品信息
    EntMallSku entMallSku = entMallSkuService.getById(skuId);
    if (entMallSku == null) {
        throw new BusinessException(CommonCodeMsg.DATA_NOT_FOUND);
    }
    // 实时校验商品库存
    SkuNumDTO skuNumDTO = new SkuNumDTO();
    skuNumDTO.setSkuId(Long.parseLong(entMallSku.getSupplierSkuId()));
    skuNumDTO.setNum(goodsNum);
    JdAddressDTO jdAddressDTO = new JdAddressDTO();
    BeanUtils.copyProperties(addressDTO, jdAddressDTO);
    List<NewStockResp.SkuStock> newStockById = jdStockRemoteApi.getNewStockById(EnterpriseApiRedisCacheUtil.getToken(),
            Arrays.asList(skuNumDTO), jdAddressDTO);
    if (CollectionUtils.isEmpty(newStockById)) {
        throw new SystemErrorException();
    }
    NewStockResp.SkuStock skuStock = newStockById.get(0);
    if (!skuStock.hasStock()) {
        throw new BusinessException(EnterpriseApiErrorCodeEnum.GOODS_SELL_OUT);
    }
    // remainNum = -1,表示未查询到
    if (skuStock.getRemainNum() > 0 && skuStock.getRemainNum() < goodsNum) {
        throw new BusinessException(EnterpriseApiErrorCodeEnum.GOODS_NOT_ENOUGH_STOCK);
    }
    // 构建购物车对象
    EntMallCart entMallCart = new EntMallCart();
    entMallCart.setId(idGenerator.generate());
    // sku相关信息
    BeanUtils.copyProperties(entMallSku, entMallCart);
    entMallCart.setUserSn(userSn);
    entMallCart.setGoodsNum(req.getGoodsNum());
    entMallCart.setIsDelete(Boolean.FALSE);

    // 更新用户购物车
    entMallCartService.addOrUpdateGoodsNum(entMallCart);
}

看到上面的方法,你一定会”头晕“吧。当我们提炼方法之后:

/**
 * 添加购物车
 * @param userSn
 * @param req
 */
public void add(Long userSn, CartAddReq req) {
    Long skuId = req.getSkuId();
    // 获取sku商品信息
    EntMallSku entMallSku = entMallSkuService.getById(skuId);
    if (entMallSku == null) {
        throw new BusinessException(CommonCodeMsg.DATA_NOT_FOUND);
    }
    // 实时校验商品库存
    validStock(entMallSku, req.getGoodsNum(), req.getAddressDTO());
    // 构建购物车信息
    EntMallCart entMallCart = buildEntMallCart(userSn, req, entMallSku);
    // 更新用户购物车
    entMallCartService.addOrUpdateGoodsNum(entMallCart);
}

/**
 * 校验商品库存
 * @param entMallSku sku信息
 * @param goodsNum 商品数量
 * @param addressDTO 地址信息
 */
private void validStock(EntMallSku entMallSku, Integer goodsNum, AddressDTO addressDTO) {
    Long supplierId = entMallSku.getSupplierId();
    // 商品库存校验
    SkuNumDTO skuNumDTO = new SkuNumDTO();
    skuNumDTO.setSkuId(Long.parseLong(entMallSku.getSupplierSkuId()));
    skuNumDTO.setNum(goodsNum);
    JdAddressDTO jdAddressDTO = new JdAddressDTO();
    BeanUtils.copyProperties(addressDTO, jdAddressDTO);
    List<NewStockResp.SkuStock> newStockById = jdStockRemoteApi.getNewStockById(EnterpriseApiRedisCacheUtil.getToken(),
            Arrays.asList(skuNumDTO), jdAddressDTO);
    if (CollectionUtils.isEmpty(newStockById)) {
        throw new SystemErrorException();
    }
    NewStockResp.SkuStock skuStock = newStockById.get(0);
    if (!skuStock.hasStock()) {
        throw new BusinessException(EnterpriseApiErrorCodeEnum.GOODS_SELL_OUT);
    }
    // remainNum = -1,表示未查询到
    if (skuStock.getRemainNum() > 0 && skuStock.getRemainNum() < goodsNum) {
        throw new BusinessException(EnterpriseApiErrorCodeEnum.GOODS_NOT_ENOUGH_STOCK);
    }

}
/**
 * 构建购物车实体类
 * @param userSn
 * @param req
 * @param entMallSku
 * @return
 */
private EntMallCart buildEntMallCart(Long userSn, CartAddReq req, EntMallSku entMallSku) {
    EntMallCart entMallCart = new EntMallCart();
    entMallCart.setId(idGenerator.generate());
    // sku相关信息
    BeanUtils.copyProperties(entMallSku, entMallCart);
    entMallCart.setUserSn(userSn);
    entMallCart.setGoodsNum(req.getGoodsNum());
    entMallCart.setIsDelete(Boolean.FALSE);
    return entMallCart;
}

分解临时变量 (Split Temporary Variable)

如果临时变量被赋值超过一次(非循环变量及收集结果等情况),就意味着它们在函数中承担了一个以上的责任。这会令代码阅读者糊涂。这种情况就应该被替换(分解)为多个临时变量。

引入解释性变量(Introduce Explaining Variable)

将复杂的表达式(或其中的一部分)的结果放进一个临时变量,以此变量名称来解释表达式的用途。简单来说,就是通过命名来说明表达式的作用。

一些复杂的逻辑运算,我们不容易看出这个逻辑想表达的意思。 比如

if (entMallSku.getStatus() == SkuStatus.SHELF && skuStock != null && skuStock.hasStock()) {
    validEntMallCartList.add(entMallCart);
} 

如果我们逻辑运算的结果赋值给临时变量,通过变量名就可以清晰的知道这个表达式的作用。

改后代码:

Boolean isShelf = entMallSku.getStatus() == SkuStatus.SHELF;
Boolean hasStock = skuStock != null && skuStock.hasStock();
// 商品上架且有库存
if (isShelf && hasStock) {
    validEntMallCartList.add(entMallCart);
}

参考

[0] 《重构,改善既有代码的设计》

[1] 《细思极恐-你真的会写java吗》