Reinventing the wheel-talk about new solution involved

所谓新方案,包括方法和技术两方面;相对于“旧”而言,“新”不代表就是自己去创造,而更多的是采用已有的更成熟、更“时髦”的工具或方法。涉及到编码话题,大到架构方案选型,小到具体某块代码都在考虑新方案。

在采用新方案时,我们必须正视一些原则,其实这些原则随处可“见”,而且基本潜移默化都会如此执行,但是这里还是以自己的切身感受重复总结一番:

(1)是否可以不引入。

每引入新的方案,带来的收益是否值得,基本也就一个简单问题:收益>支出?

有一些因素可能会让你避免引入新方案:

1.1 引入新方案就意味着引入新的风险,无新引入则无新风险,有则有新风险。

1.2 有损优势,例如有的项目以简洁见“长”(例如influxdb-java),尽量不引入额外的jar作为一个原则,仅仅为了1处复杂度不高的代码少写几行,以后估计别的地方也不会用,则引入额外的jar则得不偿失。

1.3 有应用新方案的时间,可以去做更有意义或更高优先级的事情。

(2)如果改,改成哪种?

2.1 采用的方案是否收费,版权如何规定的?

在选择时,收费和版权问题基本具有是“一票否决权”。

同时要意识到收费的方案虽“好”,但是也暗含着一定的“封闭性”。

2.2 采用的方案是否成熟?

包括是否有大规模应用案例,是否有活跃的社区支持。

2.3 采用某种方案的什么版本?

什么版本是稳定版,什么是“试验”版,例如netty有v3,v4,v5三个版本,5是试验版,但是一些喜欢“尝鲜”的工程师应用到产品中,结果悲剧发现,某天v5彻底宣布不维护了,而一些喜欢尝鲜的工程师后知后觉发现不维护后,戏称要拿刀砍死源作者!情何以堪!

2.4 所采用的版本有没有什么不能接受的问题?

基本所有的方案都会提及自己的问题, 例如git上的issues, doc更完善的方案会将issue和版本关联起来。使用某个版本时,一定要查看当前的issues,同时在具体使用中,一定要看看是否有提及“不稳定”因素等。

例如:在guava中,有一个实用的方法Splitter:


Splitter.on(",").withKeyValueSeparator('=').split("boy=tom,girl=tina,cat=kitty,dog=tommy");

用来先以,分割,然后以=分割,这个方法感觉挺酷的,正好可以解决去解析格式是key1=value1;key2=value2结构,但是用的过程中,就会发现偶尔会失败,最后定位到因为某个value是编码过的,有时候会含有分割符=,但是guava对于这种情况的处理,是直接抛异常。

这在它的javadoc中并没有说明这种情况,仅仅是在类中标了下@beta表明其有可能有bug(实际上翻阅历史,有2个bug导致其一直挂着@beta标记). (已提交1个pr去fix: https://github.com/google/guava/pull/2663,不过不知道什么时候能够merge)

大家可能说,这些都是废话,因为你要用那个方法,你不去看javadoc,不去彻头彻尾了解源码么?实际上,大家都懂的:

(1)有的方法起的太好了,以致你觉得根本不需要看文档;

(2)javadoc写不写一回事,清楚不清楚是另一会事,不是你想看就能看,想看明白就明白;

(3)就算翻源码,并没有多少时间,毕竟你要用的所有东西都有源码,根本翻不完。哈哈

2.5 持续观察

不管是什么大神级项目,都有bug,只是触发不触发到的问题,所以持续观察,不仅可以了解方案的发展和局限,也帮助预知一些问题。

使用mybatis,不言而喻其广泛使用度,最近发现一个问题,当要查询的procedure返回游标类型且为null时,报NPE错误。实际上不考虑procedure本身有没有问题,这种情况需要处理:“一个列表没有数据应该是空,但是有时有的人就要搞成null”;

Mybatis的处理,导致这种情况下不是返回null或者空,而是直接抛异常,这隐藏了一些问题,或者让人误解,因为找不到就是找不到,是正常情况。


fail  for: org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:

### Error querying database.  Cause: java.lang.NullPointerException

### The error may exist in sqlmaps/sqlmap.xml

### The error may involve project.procedure

### The error occurred while handling results

### SQL: {call xxx.YYY.call(    ?,    ?,    ?,    ?,    ?,    ?,    ?,    ?,    ?   )}

### Cause: java.lang.NullPointerException

产品一直work良好,只是有一些错误log,开始以为是产品的bug,后来debug到mybatis的一个bug,而这个bug的fix日期是2015/11/13.  https://github.com/mybatis/mybatis-3/commit/2d6aed5d3d0cb0dd1290cf520dfdc52d89e63b3f#diff-5372ad5ca04a1c3dcfd0a43546bf40ef, 让人情何以堪。而当时引入mybatis时,应该是在2014年末。所以不能认为你用了就一劳永逸了,可能是你还没有触发,或者你观察的不够。时不时跟踪注意下,就可能有新的发现。

总结:

编码就是不断使用轮子的过程,今天的新轮子可能就是明天的旧轮子。所以采用新方案要追“星”但是不能盲目追“新”。

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

 

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