花了一天时间将《编写可读代码的艺术》读完,这边对书中提到的知识点做下总结。
《编写可读代码的艺术》 的核心在于通过各种方式实现代码的高可读性。
那么怎么评判可读性?
你只要找一个对项目一点都不了解的人(但至少要有一定的编程知识),然后看他需要多长时间理解这段代码并可以做修改,而这个时间的长度就是可读性的高低。
怎么提高代码的可读性?
书中通过三个层面分别进行讲解:
- 书面层面
- 流程控制层面
- 代码结构层面
书写层面
命名
* 类命名
* 字段命名
* 方法命名
好的命名可以带来什么好处?
- 更容易记忆
- 提供更多信息
相比于字段 a
来说字段 age
能够更让人记忆深刻,而且提供了更多的信息。age
的含义是年龄的意思,而 a
你能说明它代表什么意思吗?
但是 age
提供的信息还不够完整,它到底是谁的年龄呢? 学生? 老师 ? 还是男孩?这个时候有两种方式,一种是直接在字段上体现:
// 学生年龄
int studentAge;
或者是联系上下文看它属于什么对象:
// 学生实体类
public class Student{
int age;
}
相对于类和属性来说,方法上的有效命名性价比更高。通过好的命名你就知道这个方法的作用了,而不需要深入代码中研究半天才能理解这个方法的作用。
比如方法 sendUserRegistrationInfo
比起 send
来说更能让调用者理解这个方法的作用,前者能很明显的告诉调用者这是一个发送用户注册信息的方法,而后者需要调用者自己研究下到底发送了什么信息。
好坏命名的区别
我们知道了命名的好处,但是为什么我们经常感受不到这些好处呢?是因为命名也有好有坏,不好的命名不仅没有带来这些好处,相反还给我们增加了额外的负担,如:
- 名字过长
- 空泛的命名
- 多重含义的命名
名字过长
名字过长会造成记忆负担,而造成这种情况的原因一般如下:
* 1.对于所要做的事情没有清晰的认识
* 2.所做的事情太多导致不能清晰的表达
对于第一点来说我的建议是在重新整理下需求,对于第二点的解决方法是将它拆分成多个方法,每个方法只做一件事。
空泛的命名
像 a
、b
、c
这种没有含义的命名实在是没有出现的必要,但是在 for
循环中的 i
为什么不会引起这种问题呢?
原因是因为它的作用域很小,并且它的使用已经算是一种约定俗成的习惯,不会引起使用者的误解。
多重含义的命名
多重含义的命名带来的就是容易让人误解,到底我这个方法是要做什么行为呢?在不同的语境下相同的词常常会南辕北辙。如果你想不到好的词的话,留下一段注释可能也不失为一种解决办法。
注释
注释的作用如下:
- 对代码行为做一个概括描述
- 对代码未体现的事情做个描述
- 对未完成的事做备注
基于以上两点可以看出好的注释的特征:
* 1.不是所有的行都需要注释
* 2.代码可以自体现的就不需要额外的注释(类、方法上面还是建议有概括性注释)
* 3.不同逻辑间可以做一个概括性描述,让人更容易理解代码逻辑
代码格式
代码格式对于可读性来说很重要,幸好目前的 IDE 都有智能格式化代码样式功能,所以这点对我们来说不需要太多的关注。
简化循环
我们先来看看常用的流程控制逻辑语法:
- if else
- for
- do .. while 和 while
- 三目表达式
if else
程序中 if else
判断自然是避免不了的,但是多重嵌套的 if else
判断实在不是一种可读性很好的编写方式:
Order order = orderMapple.selectByPrimaryKey(orderId);
if(order != null){
List<OrderItem> orderItemList = orderItemMapple.selectByPrimaryKey(orderId);
if(orderItemList!=null && orderItemList.size()>=0){
.............
}
}
上面的代码实现了根据订单号获取订单信息,如果订单信息存在的话就获取订单行项目列表,如果订单行项目列表存在的话,就进行一系列的操作。
可以看到这样的多重嵌套在项目中是很频繁的操作,但是如果 “一系列” 操作逻辑比较多的话,一个屏幕的空间都显示不下这个 if
作用域。对于使用者的记忆会有比较大的负担,我们可以采用逆向思维方式的方式来改变下这个代码的结构:
Order order = orderMapple.selectByPrimaryKey(orderId);
if(order == null){
return ;
}
List<OrderItem> orderItemList = orderItemMapple.selectByPrimaryKey(orderId);
if(orderItemList==null || orderItemList.size()<0){
return ;
}
.............
可以看到采用这种结构就将多重 if
判断结构转为一重 if
判断,是不是更清晰点呢?
for
继续我们之前的例子,如果订单行项目是多行的话,我们通常使用 for 循环来取值进行操作:
List<OrderItem> orderItemList = orderItemMapple.selectByPrimaryKey(orderId);
OrderItem orderItem = null;
for(int i = 0,size = orderItemList.size; i < size; i++){
orderItem = orderItemList.get(i);
......
}
我们尝试另一种风格的 for
循环看看:
List<OrderItem> orderItemList = orderItemMapple.selectByPrimaryKey(orderId);
for(OrderItem orderItem : orderItemList){
......
}
是不是比第一种可读性更好呢?当然选择哪种风格的 for
循环可以根据使用场景来决定,有时候第一种风格可能更好。
do .. while 和 while
do .. while
和 while
的功能是一样的,唯一的区别在于 do .. while
不管条件是否成立都会执行一次作用域里的代码,而 while
必须条件成立才会执行。
通常我们都会从前往后读取代码,而 do .. while
会让人重复读两边代码(因为条件在最下面,导致会在看到条件后再重新读一遍逻辑)。所以书中建议最好不要使用 do .. while
而采用修改 while
条件的形式。
三目表达式
三目表达式是为了避免写如下代码而出现的:
if(a > b){
return a;
}else{
return b;
}
三目表达式形式如下:
return a > b ? a : b;
可以看到三目表达式更简洁,但这是针对于表达式比较简单的逻辑,如果是逻辑比较复杂的可读性会变的相当差:
return a > b ? xxx(a,z) : yyy(b,z);
在这里,三目表达式已经不只是从两个简单的值中做出选择,可读性已经变得不是很好。针对这种情况采用 if else
的方式会更好,虽然代码量增加了,但是可读性同时也增加了。
结构层面
- 拆分超长的表达式
- 让方法只做一件事
- 复用代码实现少些代码
拆分超长的表达式
if(orderType != "0021" && orderType != "0022"
&& orderType != "0033" && orderType != "0034"
&& orderType != "0043" && orderType != "0044"){
......
}
如上所示,在项目中应该也会经常看到这种代码吧,单据类型不同所走的逻辑也不同。我们来做下调整:
if(isOtherOrder(order.getOrderType())){
......
}
public boolean isOtherOrder(String orderType){
List<String> orderTypeList = new ArrayList();
orderTypeList.add("0021");
orderTypeList.add("0022");
orderTypeList.add("0033");
orderTypeList.add("0034");
orderTypeList.add("0043");
orderTypeList.add("0044");
return orderTypeList.contains(orderType);
}
是否可读性更好了,而且之后如果单据类型增加了,也只需要在 isOtherOrder
方法中增加即可。
让方法只做一件事
在之前我们讲命名过长的时候就已经讲到了要让方法只做一件事情。一方面是可以之后的复用,另一方面是为了更好的明确职责。
复用代码实现少些代码
减少程序 bug
最好的方式就是不写代码,当然这是不太可能的事。为了尽量达到这个目的,复用代码是不可缺少的一件事。