写了这么多年的代码,最近看了一本叫做《代码整洁之道》的书籍,看完之后还是蛮有感悟的,特此记录下从书中学习到的一些内容。
整洁的代码
来让我们思考一个问题,什么样的代码才算是好代码?
关于这一点我大概整理了一些自己的看法,罗列了之后如下所示:
准确性
可以通过测试用例,满足具体应用场景。
简介性
没有重复性的代码,简洁明了,没有过多的繁琐类的封装,函数,方法。
看到这里,你可能会想,说起来很能理解,但是实际工作中又该如何注意呢?别急,我们来看下边这个反面案例:
@Override @Transactional(rollbackFor = Exception.class) public PayResultDTO doPay(PaymentOrderDTO paymentOrderDTO) { if (paymentOrderDTO.getAccountId() == null) { return PayResultDTO.payFail("参数不能为空"); } CartDTO cartDTO = cartService.getMyCart(paymentOrderDTO.getAccountId()); if (cartDTO == null) { log.error("【dopay】 购物车为空,支付异常 accountId is {}", paymentOrderDTO.getAccountId()); return PayResultDTO.payFail("购物车不存在,支付异常"); } List<ProductDTO> productDTOList = CommonUtil.getProductListFromCart(cartDTO); for (ProductDTO productDTO : productDTOList) { boolean isEnough = productService.checkStock(productDTO); if (!isEnough) { log.error("【dopay】 库存不足, productDTO is {}", productDTO); return PayResultDTO.payStockNotEnough(productDTO); } } PaymentOrderDTO waitPayOrderDTO = buildWaitPayOrderDTO(paymentOrderDTO.getAccountId(), productDTOList); PayResultDTO payResultDTO = paymentOrderService.insertPaymentOrder(waitPayOrderDTO); log.info("【doPay】插入订单信息,paymentOrderDTO={}", waitPayOrderDTO; if (!PayResultDTO.PAY_SUCCESS_CODE.equals(payResultDTO.getCode())) { return PayResultDTO.payFail(ORDER_CREATE_FAIL); } //处理微信签名以及校验 AccountDTO accountDTO = accountService.getAccountDTO(paymentOrderDTO.getAccountId()); WXPayReqDTO wxPayReqDTO = WXPayReqDTO.builder() .appId(MiniProgramConstants.APPID) .openId(accountDTO.getOpenId()) .mchId(MiniProgramConstants.MCH_ID) //单位是 分 .price((long) (waitPayOrderDTO.getActualAmount() * 100)) .orderNo(waitPayOrderDTO.getOrderNo()) .attachParam(waitPayOrderDTO.getOrderNo()) .body(MiniProgramConstants.DEFAULT_PAY_BODY).build(); PayResultDTO wxPayResult = wxPayService.doPayReq(wxPayReqDTO); if (PayResultDTO.isPaySuccess(wxPayResult)) { return wxPayResult; } return PayResultDTO.payFail(REQ_WX_ERROR); }
这段代码是早期我在工作时所写的一段支付代码,在进行支付之前,先判断购物车里的商品库存是否充足,如果充足则进行扣减操作。但是现在回过头来看这段代码,你就会发现存在一些问题。
- 代码优化分析 首先上边的代码在业务含义上并没有什么的问题,不过我们可以在代码的精炼程度 上进行一些优化。
对于参数校验,购物车校验,库存校验 ,其实我们都可以加入一些类似于断言组件之类的设计进行优化。
另外一些对象构建的代码其实可以做一些封装,因为调用这个接口的同事大多数时候都不会太愿意去关心这些参数的构造过程,更多时候我们只需要将它封装起来,然后通过一个函数的名字来告诉使用者就可以了。
进行优化之后的代码可以变成如下所示:
@Override @Transactional(rollbackFor = Exception.class) public PayResultDTO doPay(PaymentOrderDTO paymentOrderDTO) { //使用断言进行参数判断 BizAssertUtils.isNotNull(paymentOrderDTO.getAccountId(), ARG_ERROR); BizAssertUtils.isNotNull(cartService.getMyCart(paymentOrderDTO.getAccountId()), CART_NOT_NULL) List<ProductDTO> productDTOList = CommonUtil.getProductListFromCart(cartDTO); BizAssertUtils.isTrue(this.isStockEnough(productDTOList)); //插入预先支付订单,日志记录可以放在函数里面 PaymentOrderDTO waitPayOrderDTO = buildWaitPayOrderDTO(paymentOrderDTO.getAccountId(), productDTOList); PayResultDTO payResultDTO = paymentOrderService.insertPaymentOrder(waitPayOrderDTO); BizAssertUtils.isTrue(payResultDTO != null && !payResultDTO.isSuccess(), ORDER_CREATE_FAIL); //进行支付操作 PayResultDTO wxPayResult = wxPayService.doPayReq(this.buildWXPayReqDTO(paymentOrderDTO.getAccountId(), waitPayOrderDTO)); BizAssertUtils.isTrue(wxPayResult.isPaySuccess(), REQ_WX_ERROR)); return wxPayResult; }
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
有意义的命名
函数命名
有的时候,我们对于函数的命名会有出现“名不副其实”的情况,例如下边这个案例:
public Map<String,Integer> getResultList() { //... }
这段代码中,返回的类型是Map类型,但是函数的命名却以List结尾,这样就给人有误解的意思。如果函数的名字没法给人很好的易读性,就需要开发者再花费额外的时间趣深入理解,这就会显得很费劲。再看下边这个案例:
public long[] getList(int len) { long[] data = new long[len]; //随机填充数据 for (int i = 0; i < len; i++) { data[i] = new Random().nextInt(5); if (data[i] < 0) { data[i] = 0; } } return data; }
在这个函数里面,会自动生成一个随机数组,但是数字的值如果是负数,则会自动变为0。但是为什么我们不在函数命名中进行定义呢?例如定义命名为:getNonnegativeArr(int len), 这样不是更好理解函数的意思嘛。
变量命名
对于函数变量的命名不要使用0或者o,1或者l很容易让人看混,另外变量或者参数的命名也尽量使其具有意义,例如下边这个案例所示:
public boolean isSubList(List<String> list1, List<String> list2) { for (String item : list1) { if (!list2.contains(item)) { return false; } } return true; }
例如这个案例中,参数命名采用了list1,list2,就很让人容易误解,不知道哪个参数是源集合,哪个参数是需要被比较的子集合。如果我们对它进行一些优化,看起来的效果就会明显不同,例如:
public boolean isSubList(List<String> sourceList, List<String> compareList) { for (String item : sourceList) { if (!compareList.contains(item)) { return false; } } return true; }
使用好的变量命名可以让人清晰了解到这个变量的类型是什么样的,另外在定义一些具有特殊含义的变量时候可以结合下具体的业务场景。例如下边这段代码,需要分别计算工作日 和 周末的订单流水金额总数,
int j = 7; for(int i=1;i<=j;i++) { //周末的逻辑处理 if(j-i<2){ //... } else { //... } }
其实在这里我们可以赋予一个特殊的业务命名来表示,例如下边这样的写法:
int TOTAL_DAYS_PER_WEEK = 7; for(int dayOfWeek=1; dayOfWeek <= TOTAL_DAYS_PER_WEEK; dayOfWeek++) { //周末的逻辑处理 if(TOTAL_DAYS_PER_WEEK - dayOfWeek < 2){ //... } else { //... } }
相对于上边的变量i,j写法,这种具有业务含义的变量命名更加能让人明确它的具体含义。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
函数的定义
函数的命名
函数的命名尽量保证一个函数只做一件事的原则,不要参杂过多的其他业务操作。
函数的参数个数
通常参数的个数建议最多设置在3个左右,如果参数过多容易让人看了有误解,此时可以尝试用一些类去进行封装。
函数的参数命名
对于有共同含义的参数,我们可以将它命名到同一个变量中,这样能够通过业务领域去定义他们,方便识别。
对于参数的命名可以结合一些工作术语,例如当我们需要定义一个队列参数的时候可以定义JobQueue。
再来看下边这个案例,我们定义了一个用户基础属性类:
public class UserInfo { private long id; private String username; private String telNo; //用于表示地址信息 private String name; //居住全地址 private String number; //楼房号 private String street; //街道名称 private String city; //城市名称 }
在这个案例中的name,number,street,city字段分别表示了用户所居住的信息,但是如果我们不加以注释或者没有了解的同事和使用者预先说明的话,这些字段的具体含义是很难看出来的。这种时候我们可以尝试给它加上一些特殊的业务前缀来表示,例如下边两种写法:
class UserInfo { private int id; private String username; private String telNo; //用于表示地址信息 private String addressName; //居住全地址 private String addressNumber; //楼房号 private String addressStreet; //街道名称 private String addressCity; //城市名称 }
不过更好的表示方式还是单独设计一个对象来存储这些具有明确业务含义的对象,例如定义一个Address对象来管理它们,例如下边所示:
public class UserInfo { private int id; private String username; private String telNo; //用于表示地址信息 private Address address; } public class Address { private String addressName; //居住全地址 private String addressNumber; //楼房号 private String addressStreet; //街道名称 private String addressCity; //城市名称 }
注释的规范
不要太依赖于注释
关于代码注释方面,相信工作了一段时间的开发都会有以下想法:不太相信代码注释所写的内容。为什么会有这样的想法呢? 我感觉主要原因还是和代码的不断迭代有关,随着代码的不断改动,其内部的流程早已经和注释表达的含义背道而驰。所以有时候我们代码注释的意义并不是特别大。
有时候如果代码注释和核心内容不符的时候,它反而成了一段“谎言”的存在。所以我认为:唯一真正好的注释是你要想办法去不用写注释。
别写废话注释
在我早期工作的时候,这个点确实经常会犯,例如定义一个对象的时候,我几乎会给每个字段都加入一行注释,例如下边这个类:
public class UserInfo { private int id; //用户名 private String username; //手机号 private String telNo; //性别 private Integer sex; //年龄 private Integer age; //身份证号码 private String idCardNo; //邮件 private String email; }
虽然说给每个字段都备注了注释,看起来也似乎很规范,但是这样的做法反而给人感觉写了一堆废话。通过每个字段的命名来看,难道它们还能有别的意思嘛?而且时间久了,别人看到这些无用的注释之后也会自动过滤掉它们的含义。
代码的长度
通常我们定义的一个类,其包含的代码量不建议设计得太大,大部分都浓缩在500行以内这个范围就好了。这是因为通常短的内容会比长的内容更方便让人理解,这就好比现如今的人更喜欢刷短视频,但是对于一些长达几十分钟或者几个小时的短片/电影却没有那么感兴趣。
除了好阅读之外,在实际工作中,大家如果使用的电脑配置不高,在电脑运行了很久的情况下再去用编辑器去修改一些篇幅非常大的类,反而会塌瘪慢。(我试过用Idea打开一份3000+行数的代码文件,挪动一格光标大概会有1秒的延迟)
另外,也并不是说代码的长度越短越好,有时候适当的空格与换行可以增加代码片段的可读性。
代码的对齐
在编写代码的时候,对于变量的对齐格式不同,其实也是有讲究的。来看下边两组不同代码的书写风格:
变量的名称统一对齐:
public class Obj { private Socket socket; private InputStream inputStream; private String str; private File file; private ApplicationContext applicationContext; private AnnotationConfigApplicationContext annotationConfigApplicationContext; private DubboApplicationContextInitializer dubboApplicationContextInitializer; }
变量的类型统一对齐:
public class Obj { private Socket socket; private InputStream inputStream; private String str; private File file; private ApplicationContext applicationContext; private AnnotationConfigApplicationContext annotationConfigApplicationContext; private DubboApplicationContextInitializer dubboApplicationContextInitializer; }
在工作中,我更加会倾向于使用第二种书写格式,因为它给我的感觉会让人更加清晰看到对应变量的格式类型是怎样的。而对于第一种类型而言,给人感觉对于变量的类型不是那么地关注。
你们有哪些很好的代码习惯,欢迎分享。