小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
如何写”好代码“?这是一个困扰很多开发人员的问题。当然,写”好代码“并不是一件容易的事。优雅的算法、合适的数据结构、规范的代码、面向对象设计思想、合理的设计模式、系统设计等方面的内容,都影响着写出好代码。现在,我们尝试用另外一个角度来学习学好代码:重构。所谓重构,就是改善既有代码的设计。重构既然可以改善代码的既有设计,那么为什么不在开始写代码的时候,就用重构的某些方式直接把代码写好、写完善?今天,这篇文章就以重构视角聊聊怎么写出好的方法(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] 《重构,改善既有代码的设计》