top 10 most-overlooked rules for readable code

对于如果提高代码质量,目前互联网上“充斥”着各种文章,不论是大牛还是新人都有自己的一番见解,本文只是从可读性角度分析提高代码质量的一些易忽视或难以坚持的原则,行文以一些简单示例或参与的开源软件中的一些代码示例/交流为参考。

1  查看参与项目的代码风格

代码风格是提高代码可读性的根基,从不存在一种统一的代码风格,即使对于同种语言,也存在多样流行风格:

Java Sun http://www.oracle.com/technetwork/java/codeconvtoc-136057.html)
Java Google http://google-styleguide.googlecode.com/svn/trunk/javaguide.html
c Bjarne Stroustrup http://www.stroustrup.com/bs_faq2.html
c Google https://google-styleguide.googlecode.com/svn/trunk/cppguide.html
c GNU https://gcc.gnu.org/wiki/CppConventions

在众多风格中,很难评选出最好的style,例如对于变量名的首字符是否大小写,不能说大写一点比小写好,但是如果在代码里面一会大写一会小写肯定带来糟糕的体验,所以从始至终一致至关重要。

在融入新的项目之时,一定要熟悉所加入的项目所遵从的代码风格。例如netty项目中,对于没有set方法的成员,其get方法的命名是不带get前缀,这点与很多其他项目不同,例如:

https://github.com/netty/netty/blob/3e5dcb5f3efbb26d5e6cf4cd229b03c285d62462/transport/src/main/java/io/netty/channel/WriteBufferWaterMark.java#L77

     /**
      * Returns the low water mark for the write buffer.
       */
      public int low() {
         return low;
     }

另外它的toString()方法也有自己的风格;

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder(55)
            .append("WriteBufferWaterMark(low: ") //here is ( instead of [
            .append(low)
            .append(", high: ")
            .append(high)
            .append(")");
        return builder.toString();
    }

如果认为之前的风格不一致,不管放弃自己的还是抛弃过去的,都要始终保持一致,而不是抛弃过去,直接书写自己的风格。


就近与分段

2.1 就近

“就近”的优势在于阅读者不需要保持记忆的时间太长,且不容易受干扰,常见的例子有以下两种:

(1) 声明和使用变量:在反面例子中,声明和使用userIndex之间还间隔了其他无关代码,使得阅读者在第一次遇到userIndex之后不仅拉长了短时记忆时间且受到了信息干扰。

反面例子:

int userIndex=10;
int classroomIndex=20;
getUserNameByIndex(userIndex);

正面例子:

int userIndex=10;
getUserNameByIndex(userIndex);
int classroomIndex=20;

(2)调用者与被调用者:

反面例子1:


int caller(){
call();
}

void other(){}

private void call(){}

正面例子1:


int caller(){
call();
}

private void call(){}
void other(){}

例子2: 存在多个调用者:

反面例子2:


int caller1(){
call();
}

void caller2(){
call();
}

private void call(){}

正面例子2:


int caller1(){
call();
}

private void call(){}

void caller2(){
call();
}

2.2 分段 分段能让阅读者更容易归类信息。
反面例子:


check(name == null, "name may be null")
buy(name);
pay(name);
waiting(name);
updateCache(name)
updateStorge(name)


正面例子:


check(name == null, "name may be null")

buy(name);
pay(name);
waiting(name);

updateCache(name)
updateStorge(name)


保持顺序

保持顺序可以让阅读者减少记忆负担,例如在下面的反面例子中,传入参数是abcd的顺序,但是在进一步方法调用时,采用的是dcb,这样会使得阅读者必须牢记这两个方法使用的dcb顺序是不同的,对于同种类型的参数,特别容易因记错而出现bug。

反面例子:


public void caller(int a, String b, float c, String d) {
call(d, c, b);
}
public void call(String d, float c, String b)

正面例子:

public void caller(int a, String b, float c, String d) {
call(b, c, d);
}
public void call(String b, float c, String d)

4  给自己的代码定义一个布局

在完成一个class/cpp的书写后,我们会遇到各种元素:静态成员变量、成员变量、静态成员方法、成员方法外、内部类等,合理的布局会带来阅读者良好的体验,例如保持静态成员变量在整个类的第一位:这样当阅读者想查找一个类的静态方法时,自然会首先定位到整个页面的前部分,同时一致的布局也会给阅读者良好的体验。Sun的《Java Coding Style Guide》提供了一种布局方案(如下图),相比较简单的按访问级别(Public、Protected等)更清晰。

note: 对于同种“元素”,可以按照重要性从前往后,相关系由近及远的方式进行放置。

5 惯用约定俗成”

除了java对包、类、方法名等各个元素的名称规范外,约定俗成更侧重使用的名称或者方式本身的含义,用好约定俗成的规则,则让懂得规则的人直接了解代码的功能目标:

5.1 名称的定义约定俗成

例如《Effctive Java》中对于静态构造器的命名规范:

valueOf 类型转化,返回的实例与参数
of 同上,EnumSet中使用并流行起来
getInstance 返回的实例是通过方法的参数来描述,对于单例,该方法没有参数且返回唯一实例
newInstance 同上,但确保返回的实例都不相同
getType 同getInstance, 但工厂方法不在类中
newType 同newInstance, 但工厂方法不在类中

5.2 设计模式的含义约定俗成:例如以下几种常见模式,最好采用以下关键字,让阅读者一目了然代码意图。

singleton 创建对象,对象只有一个实例
factory 创建对象
builder 创建对象,但是很可能伴随很多可选参数
template 模板实现,可能具体某个步骤的具体实现不同
strategy 同种目标,不同实现方法
proxy 隐藏具体实现,代理实现
chain 流行线式执行,可插拔某个步骤

6 合理注释和commit

6.1 注释是提高代码可读性的双刃剑,注释写的好,有助阅读者快速理解代码,但是需要注意几点:

(1)考虑注释的初衷:如果添加代码注释是因为代码没有清晰表达意图,首先需要做的是重构而非添加注释来解释。

(2)不写无用注释:SCM工具可以记录的信息都可以不去注释,例如Change Start/End, Changed by XXX.

(3)同步更新注释:注释本身也是代码的一部分,需要维护,如果修改代码逻辑,而不同步更新注释,就会造成误解。

(4)对待注释如同代码:例如对于class级别注释的首句,要求简短精炼。

nettyissue_1

用好注释能让阅读者的阅读事半功倍,如果用的不好则适得其反,所以一定要合理注释。

6.2 commit代码时要注意两点:

(1)follow一定格式:例如为什么要改,怎么改的,改后的效果和影响;

例如

fix-one-java-doc-issue_netty

(2)减少相关commit次数,squash related temp commits into one:

很多时候,不可能一次完成task,多次提交后,commit就会特别多,但是最终提交时,应尽量将commit控制在1个: 包括所有相关代码、测试、readme的changes等,如此非常清晰,让以后的维护者一目了然。

反面例子:

multi_commits

7 对称与一致

7.1 方法名定义对称性:

方法名的对称性不仅可以帮忙阅读者理解代码,而且使人在泛读代码时忽略很多信息,最常见的如set/get组,toString/fromString组,如果一份代码全是这样的组,意味着阅读者至少可以忽略一半的方法。

7.2 方法实现的对称性:

反面例子:


      preHandleOrder(order);
      printOrderInfo(order);
      if (order!= null) {
         transfer(order)
      }

正面例子:


      preHandleOrder(order);
      printOrderInfo(order);
      transferOrder(order);

新增方法:


      void transferOrder(Order order){
           if (order!= null) {
           transfer(order)
      }

7.3 命名/实现的一致性:

(1)名称: 例如经常看到在class/cpp 1中命名delete,在class/cpp 2中命名remove,尽量应该保持一致;
(2)大小写:例如ID有三种形式,id, Id, ID等,到底用哪种,统一可以避免很多困惑;
(3)实现一致:例如在redis java client项目中:

https://github.com/xetorthio/jedis/pull/1396/files

反面例子:


      if (null != this.password ) {
        jedis.auth(this.password);
      }
      if (database != 0) {
        jedis.select(database);
      }
      if (clientName != null) {
        jedis.clientSetname(clientName);
      }

正面例子:


      if (password != null) {
        jedis.auth(password);
      }
      if (database != 0) {
        jedis.select(database);
      }
      if (clientName != null) {
        jedis.clientSetname(clientName);
      }

8  处理好字符

(1)添加关键字符

FIXME 需要修正的功能 JDK
XXX 需要改进的功能 JDK
TODO
JDK
@Beta beta版 guava
@VisibleForTesting 为了测试目的提高可见性 guava
 @UnstableApi  不稳定  netty

(2)消除魔术字符

9  完善代码不是一味精简代码:冗余与精简结合

秉承少即是多的原则,一方面要坚持用更少的代码来表达,另外一方面可以适当增加一些代码行来提供可读性。

9.1 精简代码:主要包括二方面:

(1) 删除无用代码:这主要包括以下几个方面:

废弃的功能  整个功能不再需要,但是代码还没有被注释
注释掉的代码  不用了,又怕删了找不回来,所以临时注释了,但一直未清理
临时的一些测试方法  最常见的莫过于加一个main方法然后写点测试
冗余重复的逻辑 https://github.com/influxdata/influxdb-java/pull/233

(2)用新语法/新方法更有力的表达:随着语言本身或第三方类库的发展,会添加不少“语法糖”和新的实用方法,这促使可用更简短、稳定的方式来替换精简代码,例如JDK8新引入的lambda表达式:

Collections.sort(domainDOList, new Comparator<DomainDO>() {
@Override
public int compare(DomainDO o1, DomainDO o2) {
return o1.getPriority() - o2.getPriority();
}
});

//change to 
Collections.sort(domainDOList,(o1, o2) -> o1.getPriority() - o2.getPriority());
for (URI uri : uris) {
if (uri.getPriority() == 1.0)
return true;
}
return false;
}

//change to
return uris.stream().anyMatch((uri)->uri.getPriority() == 1.0);

再如,使用guava的cache模块,替换自己实现的local cache;使用jdk8自带的base64统一其他所有纷繁众多的base64。

9.2  增加代码:提高代码质量,不意味着就是“缩减”,适当增加一些代码反而能提高代码可读性,例如引入解释性变量或者方法:

反面例子:

if (student.getAge() >= 16 && student.getAddressCityCode() == 3)
      return "can attend this game free";

正面例子:

boolean isAdult = student.getAge() >= 16;
boolean isHefei = student.getAddressCityCode == 3;
if (isAdult && isHefei)
      return "can attend this game free";

10 结束可能才是开始:自省与他审

代码写完之后,可能洋洋得意于代码设计之精妙、阅读之流畅,但是务必谨记这仅是自己的感觉,如果想代码脍炙人口,还要经过不断的自省与他审,具体修改代码的需求来源于以下四方面:

10.1 工具检查,例如checkstyle,PMD等工具查看代码风格或复杂度。

10.2  阅读其他类似经典实现,比较差距:不管代码的实现功能是什么,基本上总能找到类似实现,毕竟我们创造轮子的时间比重复轮子的时间要远远小。所以大体总是能找到类似的“专家”实现,从而比较出差距以完善。

10.3 他人的评审:他人可以是一个对业务的入门者,也可以是经验丰富的同伴,前者能反映代码的易懂性,后者能高屋建瓴,给予更多经验性修正意见。

10.4 间隔一定时间的自省:随着时间的推移,自己对之前代码会存在遗忘,同时自己的知识经验也会有一定提升,这给代码的重新自省带来了好的基础,间隔的阅读必然带来一番新的感受和体验。

 

总结:  通过以上的十条建议,我们可知,好的代码表达不仅能让阅读者轻松阅读和记忆代码,同时也能引导阅读者去养成和自己代码风格匹配的阅读习惯,从而发生共鸣。好的代码也是进一步优化代码性能的前提,适合机器执行的不见得适合阅读,但是代码更大部分消耗的时间不是创建而是维护,所以优化代码可读性至关重要。

redis analyst (1)- first try

项目上第一次引入redis来解决并发问题,所以记录下使用中体会的一些要领以备忘(以下行文以jedis作为客户端为案例):

(1)jedis最好设置下clientname,以便于trouble shooting, 但是spring-data-redis并没有提供设置方法(已提交pull request去支持:https://github.com/spring-projects/spring-data-redis/pull/219, 已merged),这点还是直接调用jedis方便。

public JedisSentinelPool(String masterName, Set sentinels,
final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
final String password, final int database, final String clientName) {
this.clientName = clientName;
….
}

note: jedis的经常用的pool都支持clientname, 但是ShardedJedisPool还不支持clientname设置,也已提交pull request: https://github.com/xetorthio/jedis/pull/1383,不知道何时可以merge(至今未merge,倒是merge了cluster模式的支持)。

这样可以通过client list来获取client的name,对以后的troubleshooting必然有所帮助,例如:获悉某时刻连接数最多的app是哪个,每个app都在执行什么命令等。


10.224.38.23:0>client list

"id=8 addr=10.224.38.30:26636 fd=11 name=sentinel-ba1e0dae-pubsub age=1350878 idle=0 flags=N db=0 sub=1 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=subscribe //set client name

id=18 addr=10.140.201.34:51637 fd=12 name= age=85650 idle=85619 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=scan // not set

(2)清理keys及key的设置规则

清理keys一般不会删除所有的,否则公用的也被删除了,所以如果一般删除的话,要求keys有规则,这要求keys的设置符合一定的规则。

以下删除了所有的keys:

@Override
public void evictAll() {
Set keySet = redisTemplate.keys("*"); //匹配了所有的key
if (keySet == null ||keySet.size()==0) {
logger.debug("no keys are found");
return;
}

this.redisTemplate.delete(keySet);
}

key设置要具备一定的规则(以:风格),这样便于管理:

set user:id:895689 “fujian”

redis001

(3)有文章评论常用的setnx执行完后,如果定义过期时间,可能会失败,导致数据永远删除不了,所以推荐pipeline之类一步将命令发出。但是目前已经有新的set方法来合并这2步操作:

原先方法:

return redisClient.execute(jedis -> {
Long setnx = jedis.setnx(meetingKey, ip);
jedis.expire(meetingKey, 30); //这边可能会出错
if (setnx == 0)
return jedis.get(meetingKey);
return ip;
});

新的方法:

return redisClient.execute(jedis -> {
String value = jedis.set(meetingKey, ip, "NX", "EX", 30);//原子操作
if (value == null)//返回的是OK或者null,区别于setnx的1和0.
return jedis.get(meetingKey);
return ip;
});

(4)db的选择,默认有16个db, 默认采用的是db0, 如果需要更改,可以修改databaseid, 有文章评论说,每次操作都需要切换,实际上只做一次就可以了。

@Override
public void activateObject(PooledObject pooledJedis) throws Exception {
final BinaryJedis jedis = pooledJedis.getObject();
if (jedis.getDB() != database) {
jedis.select(database); //仅仅select 1次足够
}

}

 @Override
  public String select(final int index) {
    checkIsInMultiOrPipeline();
    client.select(index);
    String statusCodeReply = client.getStatusCodeReply();
    client.setDb(index);

    return statusCodeReply;
  }

但是分库一定要协商好,否则贸然使用非默认的,例如2,结果配置的database数目不是3个,则和预想的不同,且需要注意,默认是16,但是可以只配置1个,不分库.

参考配置文件:

# Set the number of databases. The default database is DB 0, you can select
# a different one on a per-connection basis using SELECT where
# dbid is a number between 0 and 'databases'-1
databases 16

CMD切换方法:
select 0 #打开id为0的数据库,也就是第一个库。
redis02

源码分析:

db.c

void selectCommand(client *c) {
    long id;

    if (getLongFromObjectOrReply(c, c->argv[1], &id,
        "invalid DB index") != C_OK)
        return;

    if (server.cluster_enabled && id != 0) {
        addReplyError(c,"SELECT is not allowed in cluster mode");
        return;
    }
    if (selectDb(c,id) == C_ERR) {
        addReplyError(c,"DB index is out of range");
    } else {
        addReply(c,shared.ok);
    }
}


int selectDb(client *c, int id) {
    if (id < 0 || id >= server.dbnum)
        return C_ERR;
    c->db = &server.db[id];
    return C_OK;
}

robj *lookupKeyWriteOrReply(client *c, robj *key, robj *reply) {
    robj *o = lookupKeyWrite(c->db, key);
    if (!o) addReply(c,reply);
    return o;
}

server.c  初始化


//取默认配置多少个db
void initServerConfig(void) {
    server.dbnum = CONFIG_DEFAULT_DBNUM;  
}

//申请空间
server.db = zmalloc(sizeof(redisDb)*server.dbnum); 

//初始化
for (j = 0; j < server.dbnum; j++) {  
        server.db[j].dict = dictCreate(&dbDictType,NULL);
        server.db[j].expires = dictCreate(&keyptrDictType,NULL);
        server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
        server.db[j].watched_keys = dictCreate(&keylistDictType,NULL);
        server.db[j].id = j;
        server.db[j].avg_ttl = 0;
}

(5)redis可以配置最大memory以保护自己,超过最大memory使用的清理策略可以参考配置文件(http://download.redis.io/redis-stable/redis.conf):

# maxmemory

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru; remove the key with an expire set using an LRU algorithm
# allkeys-lru; remove any key according to the LRU algorithm
# volatile-random; remove a random key with an expire set
# allkeys-random; remove a random key, any key
# volatile-ttl; remove the key with the nearest expire time (minor TTL)
# noeviction; don't expire at all, just return an error on write operations
#
# Note: with any of the above policies, Redis will return an error on write
# operations, when there are no suitable keys for eviction.
#
# At the date of writing these commands are: set setnx setex append
# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
# getset mset msetnx exec sort
#
# The default is:
#
# maxmemory-policy noeviction

(6)redis最多支持多少连接? 及客户端pool的配置应该如何?

redis server默认最多支持1万连接。

# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the Redis server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
# minus 32 (as Redis reserves a few file descriptors for internal uses).
#
# Once the limit is reached Redis will close all the new connections sending
# an error 'max number of clients reached'.
#
# maxclients 10000

而默认的jedis pool配置如下:可知即使不启动eviction, 所有机器满负载情况下,1W/8是最多能部署的机器; 如果启用eviction的前提下,负载不大的情况下,可以部署的机器>1W/8, 所以基本可以认为,1W/maxTotal是所能部署的最多机器。如果想在负载可控的情况下提高部署机器的数量,可以启用eviction.

#pool configure
##pool basic configure
pool.maxTotal=8
pool.maxIdle=8

pool.testOnCreate=false
pool.testOnBorrow=false
pool.testOnReturn=false

pool.blockWhenExhausted=true
pool.maxWaitMillis=10000  //一定要设置,否则可能永久blocked.


## idle related configure
###timeBetweenEvictionRunsMillis -1 is not allow evict
pool.timeBetweenEvictionRunsMillis=30000
###only when timeBetweenEvictionRunsMillis>0, minIdle can work, and will close idle connection number util to minIdle
pool.minIdle=0
pool.testWhileIdle=true
###concurrent check for eviction,When negative, the number of tests performed will be ceil(getNumIdle/abs(getNumTestsPerEvictionRun)
pool.numTestsPerEvictionRun=-1
###eviction not evict idle time < minEvictableIdleTimeMillis
pool.minEvictableIdleTimeMillis=60000

##JMX
pool.jmx=true
pool.jmxNamePrefix=pool

(7) redis performance

可使用自带工具redis-benchmark
local test:

[root@wbxperf001 src]# ./redis-benchmark -p 30002 -q -n 1000000 -d 4000 -t set,get -r 100000
SET: 84402.43 requests per second
GET: 90694.72 requests per second

remote test:

[root@wbxperf001 src]# ./redis-benchmark -h 10.224.2.142 -q -n 1000000 -d 4000 -t set,get -r 100000
SET: 50782.04 requests per second
GET: 50423.56 requests per second