如何写出优雅的代码

最近在做代码审查时,发现一个问题,就是代码不够优雅。代码可以有bug,但不能不优雅,毕竟代码不只是运行程序,凡是需要维护的代码都是给人看的,你的代码风格侧面反映了你的编码习惯、思维逻辑和专业性。

如何写出优雅的代码_第1张图片

那么如何让代码更优雅?逻辑性和可读性更强?我刚工作时对这个问题也很迷茫,然后当时看了《代码整洁之道》这本书,刚读到变量命名部分就虎躯一震,这就是我要找的答案。今天我结合以往开发经历从如下几方面介绍如何让代码更优雅:

  1. 命名

  1. 注释

  1. 方法

  1. 参数传值及校验

  1. 异常捕获

  1. 重复代码

1.命名

变量和函数名要简洁、见名知意,当然长点也无所谓,整点人类能看懂的命名。

示例内容

反例

正例

例如定义商品list,命名能写明白点就不要省那俩字母,能省出来啥?

list; gList; gdList;

goodsList;

例如定义一个类型,这种毫无含义的命名,写起来很快,等自己回头看都不知道是啥玩意。

t; type1; type2; typeOne; typeTwo;

businessType; productType;

messageType;

还有挂羊头卖狗肉的命名,例如定义一个订单Map,但名字是xxList,亲眼所见。

orderMap;

orderList;

2.注释

注释的作用是告诉读者代码逻辑和意义,但经常会有很多注释在说一些废话,如果写了骚逻辑要加上注释说清楚。

反例

正例

备注

//状态

String status;

//状态 0=待审批 1=已审批

String status;

这些变量上过学的都知道什么意思,反例注释属于废话,应该把参数意义和逻辑讲明白。

//定义变量sleepFlag

int sleepFlag;

//定义变量flag,当flag=0时线程睡眠10秒,核心代码,下次优化找客户要钱

int sleepFlag;

3.方法

方法对应的是一个行为,每个方法应该独立做一件事,如果要做多件事应该通过调用多个不同方法实现,好的方法应该是高内聚、低耦合。说白了就是别把一大段错综复杂的逻辑都塞到一个方法里,按照行为拆一拆,拆出一些能复用的方法。

但会有很多情况是一个很长的Service方法写完所有逻辑,做很多事情,甚至在Controller层这样做,这样导致的结果是当时写起来简单,代码复用性差,后续扩展改造麻烦。

举一个简单的例子说明ReUse的作用:

假如有一个订单服务OrderService,订单明细OrderDetailService,商品GoogsService。我需要查询某一笔订单的商品信息。假如我无视方法独立做一件事的原则,可能会写出如下代码:

public class OrderService {

    private OrderMapper orderMapper;
    
    private OrderDetailMapper orderDetailMapper;

    private GoogsMapper goodsMapper;
    
    public List getGoogsListByOrder(String orderNo) {
        // 查询订单
        Order order = orderMapper.selectByNo(orderNo);
        if (order == null) {
            //返回订单不存在
        }
        // 查询订单明细列表
        List orderDetails = orderDetailMapper.list(orderNo);

        // 查询商品列表
        List goodsList = orderDetails.stream().map(e-> {
            return goodsService.selectById(e.getGoodsId);
        }).collect(Collectors.toList());

        return goodsList;
    }
}

我们思考以下问题:

1.假如另外也有功能需要查询订单、查询订单明细、查询商品列表功能,怎么复用?

2.如果其他Service也按这种风格引用OrderDetailMapper和GoodsMapper,后续如果发现有bug或者要加校验,怎么统一处理?只能人工一处处修改,漏改了怎么办?

3.在OrderService中直接引入其他Mapper,那这些Mapper对应的Service干啥?反过来调你的Mapper吗?你住我家,我住你家?

从上面代码的问题可以看到,代码重复率高,可复用性差。之前有些小伙伴执意写上面这种代码,我也没能说服他们,当然代码质量也不尽如人意,不知道他们现在是不是还这么执着。

如何写出优雅的代码_第2张图片

看一下调整后的代码:

public class OrderService {

    private OrderMapper orderMapper;
    
    private OrderDetailService orderDetailService;

    private GoodsService goodsService;
    
    public List getGoogsListByOrder(String orderNo) {

        selectByNo(orderNo);
            
        // 查询订单明细列表
        List orderDetails = OrderDetailService.list(orderNo);

        // 查询商品列表
        List goodsList = orderDetails.stream().map(e-> {
            return goodsService.selectById(e.getGoodsId);
        }).collect(Collectors.toList());

        return goodsList;
    } 

    public Order selectByNo(String orderNo) {
         // 查询订单
        Order order = orderMapper.selectByNo(orderNo);
        if (order == null) {
            //返回订单不存在
            throw new BusinessException("40001", "订单不存在!")
        }
        return order;
    }
}

public class OrderDetailService {
    
    private OrderDetailMapper orderDetailMapper;

    /**
     * 查询订单明细列表
     */
    public List list(String orderNo) {
        //这里可以统一做一些判空、规则校验和处理
        return orderDetailMapper.list(orderNo);
    }
}

public class GoodsService {
    
    private GoogsMapper goodsMapper;

    public Goods selectById(String goodsId) {
        //这里可以统一做一些判空、规则校验和处理
        return goodsMapper.selectById(e.getGoodsId);
    }
}

之前有小伙伴问我,为什么要给每个表Mapper都创建对应的Service?拿着Mapper直接调用不挺好吗?

我回答是你写的时候是挺好,省事,但好不了几天,比如上面代码里你可以在每个service方法里做一些检验和处理,如果没有做封装,重复代码多了,那就得挨个改。而且会发现重复代码复制太多,年轻人你把握不住啊,逻辑都整不明白了。所以该封装还是要封装,不要拿着mapper到处调用满天飞。

当别人拿着复制的代码愁眉苦脸改bug,无法复用,你拿过来service方法轻松一调用,逻辑都封装在service方法里,想调整很简单,这不比他们优雅?

4.参数传值及校验

参数传值:

  1. 当方法参数个数大于3个时,就要考虑封装到对象DTO里传递,不然参数列表巨长,打印日志还费劲。

  1. 不要用Map或者JsonObject作为接受参数,谁知道你这里面放了什么东西?

DTO示例如下:

@Data 
@Accessors(chain  = true) 
public class PsbcAftLoanCreateConDTO {
    /**
     * 业务流水号
     */
    @NotBlank(message = "业务号不能为空!")
    private String applCde;
 
    /**
     * 贷后变更申请编号
     */
    @NotNull(message = "贷后变更申请编号不能为空!")
    private Long signNo;
 
    /**
     * 合同环节
     */
    @NotNull(message = "合同环节不能为空!")
    private ContractLinkEnum ctrLink;
 
    /**
     * 是否重签 1=重签;0=非重签;
     */
    @Range(min = 0, max = 1, message = "是否重签只允许0和1")
    private Integer resignFlag = 0;
 
    @Override
    public String toString() {
        return "PsbcAftLoanCreateConDTO{" +
                "applCde='" + applCde + '\'' +
                ", signNo=" + signNo +
                ", ctrLink=" + ctrLink.toString() +
                ", resignFlag=" + resignFlag +
                '}';
    }
}

示例中同时使用了hibernate validation参数校验,代替如下繁琐的校验代码:

if (acctBindParam.getAccChgSeq() == null) {
    throw new BusinessException("-1", "变更流水号为空");
}
if (StringUtil.isEmpty(acctBindParam.getDataSrc())) {
    throw new BusinessException("-1", "数据来源为空");
}
if (StringUtil.isEmpty(acctBindParam.getStartInstuCde())) {
    throw new BusinessException("-1", "资金方为空");
}
if (StringUtil.isEmpty(acctBindParam.getIndivMobile())) {
    throw new BusinessException("-1", "短信接收手机号为空");
}
if (StringUtil.isEmpty(acctBindParam.getIndivMobile())) {
    throw new BusinessException("-1", "短信接收人为空");
}
if (CollectionUtils.isEmpty(acctBindParam.getApptList())) {
    throw new BusinessException("-1", "用户信息为空");
}

当然还需要统一捕获校验异常,封装为响应对象返回给前端,不再赘述。可参考之前的文章《spring参数校验消除重复代码》

5.异常捕获

使用HandlerExceptionResolver统一捕获业务异常,并转换成带有错误码和错误提示的ModuleAndView返回给前端。

千万不要在方法里层层返回进行判断,大概就是下面这个样子,代码耦合,扩展性差:

如何写出优雅的代码_第3张图片

统一异常捕获是如何做到解耦呢,大体就是这么个意思:

如何写出优雅的代码_第4张图片

6.重复代码

重复代码让维护者深恶痛绝,在IDEA里阿里巴巴规约也会给重复代码标注晃眼的波浪线,那么为什么重复代码还是这么泛滥呢?

也许是因为对于开发者来说这是最省事的写法,但是,出来混总是要还的,除非你跑路够快,否则还是免不了后期去维护这些重复代码。而且一时的懒惰带来的是短期的舒适,但失去的是思考和成长的机会。

举个栗子,从下图可以看到若干个uploadXXX方法里面有大量的重复代码,如果这些代码出问题需要维护,那么就需要挨个找到重复代码逐个修改,重复代码越多,代码逻辑就越乱,维护成本更高,改错、漏改风险极高。

在实际开发中一定不要偷懒,把基本行为封装通用方法,通过封装消除重复代码,代码逻辑会更清晰,更容易维护。

如何写出优雅的代码_第5张图片

你可能感兴趣的:(Java,java)