什么是clean code
所谓的clean code 字面上就是整洁代码的含义,落实到我们工程师日常coding中就是如何写出看上去干净、逻辑清晰、有一定抽象能力扩展性好的代码。
这些文字的定义显得不那么生动形象,看看下图
左侧的就是clean code,右侧的就是WTF(what the fxxk,让人看见想骂xx的意思) code。那么有的人会问clean code也有WTF,是不是还不是真正的clean code,如果你是一个追求极致的人那么这个问题没毛病。但是我想说的是没有什么代码没有0 WTF的,及时所谓非常整洁规范、干净的代码也或多或少是些小毛病。所以我们工程师能够向着0 WTF的方向去努力去优化自己的代码就是成功的。
其实clean code这个话题,可以说大小所有互联网公司的工程师在日常工作中都有所耳闻。但是clean code又如同现在的淘宝、京东首页是个"千人千面"的话题,大到一个公司小到一个团队对clean code的理解都所有不同,虽然核心思想都一样但是如何在团队中培养clean code的意识、如何在日常的CR中落实所谓clean code的规则、如何在项目快速迭代的背景下保持团队代码的整洁性、可维护性等问题可能并不是每个团队做的足够好。
下面我们围绕clean code意识、规则,如何做?这两个问题聊一聊我在工作中对这几个问题的思考以及解决思路和在团队中的落实。
clean code 意识的重要性
clean code意识,为什么我们要写干净、整洁的代码?老生常谈,最直观的感受就是你不想接手一个系统后,熟悉这个系统的代码的过程中不停的WTF!,心情抑郁、生活受到影响。虽然说的有些夸张,但是相信读者只要是看过别人写的系统代码或多或少会有那么点的感同身受。我自己就在接手一个系统的这个阶段出现过上述的症状,那感觉足够的酸爽。
举几个不那么clean的代码的例子(当然只是为了说明问题不是人身攻击):
栗子1:打开这个类就有作呕的感觉,应该思考三个问题:1k+行的类是不是过于笨重?如此多的重复代码是不是应该做抽取?如此多的"小黄屎"(idea等主流IDE自带的代码检查功能)是如何堆积到现在的?
栗子2:可能大家一眼看不出啥问题,或者说这么短的一个方法能有啥问题 及时有问题影响又不大一个打日志的功能而已。
如果是前者(一眼看不出啥问题)那么我可以解答下 这个方法虽然短,但是问题多多,首先从方法命名上看不出是干啥的虽然我们可以用注释去描述但是见名知意是最基本的要求。另外CommonConfig.isOnlient()的方法已经描述清楚了是否是线上的语义,那么//线上 //线下 这两行注释就显得那么的鸡肋,其实鸡肋还好 万一开发手抖了写反了 那么这个就上升到各个说的 WTF!的高度了。
如果是后者(无关紧要),我觉得应该回炉再教育。知微见著吧,正是要有对你每一行代码的足够的负责、苛刻的态度和意识才是一个真正意义上的工程师。
public void logPlatInter(String paramStr) {
if(CommonConfig.isOnline()) {
//线上
this.logInfoFilter(paramStr);
} else {
//线下
this.logInfoObj(paramStr);
}
}
栗子3:这是业务线里一段线上代码,这段代码的直观感受是代码简单 就是些if、switch但是死活看不懂。问题出在哪里呢?我想很有可能代码的第一版作者这样去写也没有问题 因为当时的场景优先也没有想到这个逻辑会写的如此复杂,后续的开发在迭代该功能的时候也是想着就加个分支也不想去动这块 优化重构下 先完成功能嘛。正是因为抱有这样的思想去做系统 系统才会越做越复杂 越来越难以维护。试想如果后续的开发能够及时去重构掉这块逻辑 那么也不至于逻辑积累到现在这样。这就是为什么要及时重构的重要性。这段代码逻辑复杂到几乎不可测试了,试想QA同学需要多少时间去看懂 更需要多少时间去完成这块代码逻辑的回归。这样从开发、测试到最终的上线验证代价实在是太大了!
方法行数超过了180行!
private static PriceTag compute(PriceType type, int price, int viewPrice, int specialOfferProfite, int insurancePrice, BigDecimal insurancePromotionAmount) {
boolean isFireSale = SpecialOfferUtil.isFireSaleOpen();
String firePrice = ConstantsCommon.getSysCongfigValue("FIRE_SALE_PRICE_PERCENT");
if ("2".equals(ConstantsCommon.getSysCongfigValue("QUOTATION_METHOD", null))) {
PriceTag result = empty(type, viewPrice);
switch (type) {
case N:
case A :
case Q :
case S:
if (showPr())
result = general(type, InsuranceUtils.getCutingPrice(price, viewPrice, insurancePrice, type, insurancePromotionAmount), viewPrice, specialOfferProfite);
break;
case SC:
if (showPr())
...;
break;
case NC:
if(TagUtils.isNc(type)){
if (showPr()){
...;
}
break;
}
case TN :
if (showPr()){
...;
}
break;
case L:
if (showPr() && isFireSale){
...;
}
break;
case NI :
case AI :
case QI :
case SI:
if (showIpr())
...;
break;
case SCI:
if (showIpr())
...;
break;
case TNI :
if (showIpr()){
...;
}
break;
case NCI:
if(TagUtils.isNc(type)) {
if (showIpr()){
...;
}
break;
}
case LI :
if (showIpr() && isFireSale){
...;
}
break;
case C :
int childPrice = (int) CommonUtilCommon.computeChildPrice(viewPrice);
if (showPr())
result = general(type, childPrice, childPrice);
else
result = empty(type, childPrice);
break;
case CI :
childPrice = (int) CommonUtilCommon.computeChildPrice(viewPrice);
if (showIpr())
result = general(type, childPrice, childPrice);
else
result = empty(type, childPrice);
break;
default :
break;
}
return result;
} else {
switch (type) {
case A :
case N :
case S :
case B :
case F :
case SC :
case BC :
case FC :
case Q :
case TN :
if (showPr())
result = general(type, price, viewPrice, specialOfferProfite);
break;
case NC :
if(TagUtils.isNc(type)){
if (showPr())
result = general(type, price, viewPrice, specialOfferProfite);
break;
}
case L:
if (showPr() && isFireSale){
...;
}
break;
case AI :
case NI :
case SI :
case BI :
case FI :
case SCI :
case BCI :
case FCI :
case QI :
case TNI :
if (showIpr())
...;
break;
case NCI:
if(TagUtils.isNc(type)){
if (showIpr())
...;
break;
}
case LI:
if (showIpr() && isFireSale){
...;
}
break;
case C :
int childPrice = (int) CommonUtilCommon.computeChildPrice(viewPrice);
if (showPr())
result = general(type, childPrice, childPrice);
else
result = empty(type, childPrice);
break;
case CI :
childPrice = (int) CommonUtilCommon.computeChildPrice(viewPrice);
if (showIpr())
result = general(type, childPrice, childPrice);
else
result = empty(type, childPrice);
break;
default :
break;
}
return result;
}
}
例子就不多举了,其实类似的代码相信大家在工作中见怪不怪了,因为太多了。那么我们如何能够少看见类似的代码 少去制造这些让人不适的代码呢,接下来结合我自己的工作经历和所在团队的是怎么做的聊一聊如何去规避这些问题。
代码clean的方法
庆幸的是我所待过的团队,有的做的已经很好,有的虽然初期不好但是至少有向好的意识支持我去实施如何促进团队的clean code化。
开发规范,一个团队共同认可的规范是clean code的基石,大概包含以下几个部分
1、代码格式化
这个是基本,虽然简单但是是前置条件。我所说的代码格式化包含两部分,一是统一的格式化插件、模板,一是格式化的意识。有的团队忽略了这一步,后期的代价非常大!如果没有团队维度的代码格式插件、模板,团队每个人的代码那真是"千人千面"了,初看别扭最重要的多个开发共同修改一个分支 批次之前如果都去格式化共同的类 结果会非常酸爽,merge的开销非常大 直接影响团队的整体效率。
在qunar里 我们有统一的格式化模板,只需要三两步的配置就ok了
step1:下载 google 格式化代码文件GoogleStyle-qunar1_0.xml
step2:按照这篇文章中所述将格式化文件导入到idea中https://agilewarrior.wordpress.com/2015/08/07/how-to-add-intellij-google-style-guide-to-intellij/
第二就是要培养团队内开发同学格式化的意识,每次push前养成format code的习惯。
2、开发规范
这点其实就是clean code的背书了,或者像一个一个的约束细则。关于这个 我所以调研过的有两个也就是java世界国内国外的两个典范,一个是google爸爸,一个是阿里爸爸。
2.1 阿里巴巴开发手册(推荐)
阿里巴巴Java开发手册v1.2.0.pdf
2.2 google开发规范
pdf:google-java-styleguide-zh.pdf
官方英文版:https://google.github.io/styleguide/javaguide.html
中文版:http://www.hawstein.com/posts/google-java-style.html
其实阿里巴巴的开发手册可以说是google开发规范的精简、中国特色版。看得出是阿里同学在工作过程中的思考、积累,他山之石我们直接用就很好了,配套的阿里巴巴开发手册的检查插件百度一下就有配置方法,结合idea内置的代码检查已经足够明确地帮你修复问题代码,推荐下载配置。
3、cr标准
这部分是开发规范的落地阶段。每个团队必不可少的及时cr,如何在cr阶段去发现问题代码 不干净的代码是个问题。除开逻辑上的cr,代码规范、整洁的检查很重要,在我自己的工作中总结了一些标准和规范,分享出来仅供参考。
0、阿里巴巴开发手册里的所有规范
1、重复代码
重复代码、重复代码...97...、重复代码!重要的事情说100遍。遇到重复就要考虑去抽取,一时不抽 一世后悔。
2、过长方法(50行 or 一屏)
这个没的说,太长方法或者功能太多的类一定要及时进行重构,不然最受影响的是开发自己和悲催的系统交接人。
3、去掉不必要的代码注释、main方法
这个也很直接,代码是最好是自描述的,这些无用的注释或者说没必要的注释浪费磁盘空间。但是自描述这个要有度,如果是自描述不了的比如复杂的业务逻辑(这种建议进行业务重构)或者一段算法还是需要有详细的注释的。
4、合理地设置方法、变量的private、public、protected属性(先闭后开)
5、 适当地清除DE提示的代码问题(小黄屎)
基本操作,无需多言。建议不清理小黄屎的代码cr不通过。
6、使用稳定的第三方包进行代码简化 apache(lang3)、google(guava),切忌造轮子
7、lombok
这个是一个第三方的jar包,目的是让我们通过去bean的简单注释免除重复无用的代码coding。
本段摘自(https://www.ibm.com/developerworks/cn/opensource/os-lombok/)
我们看这样一个例子,一个标准的 Java bean。一个典型的 Java bean 一般具有几个属性。每个属性具有一个 getter 和 setter。通常还会有一个 toString() 方法、一个 equals()方法和一个 hashCode() 方法。
初看上去,其中可预见的冗余就已经非常多了。如果每个属性都具有一个 getter 和 setter,并且通常如此,那么又何必详细说明呢?
那么如何去使用这个lombok,它里面有哪些功能。简单google下吧。我自己也写了一个文档 公司内部的同学可以参考http://wiki.corp.qunar.com/confluence/pages/viewpage.action?pageId=156637737
4、maven规范
这个话题为什么放在clean code的话题呢,可能有点突兀。但是个人觉得非常非常重要。早在三年前 余老是在内部的培训视频上有过一个clean maven的教程,我看过 深有感触。其实我们的工程是通过maven、gradle等工程打包工具 打包部署的。如果没有一个清爽、干净有规范的pom文件,整个系统会变得笨重、臃肿,后期的维护迭代成本陡增。因此所谓的maven规范其实就是帮助大家如何去管理自己工程jar包 提出一些经验性的方法。另外建议内部同事可以去搜索一下余老师的maven课程,其实就是追求极致clean code的一个实践 值得学习研究。
总的目标:工程使用的才引入,不要copy-paste其他工程的pom文件造成大量的无用引用 使得自身工程笨重不堪
4.1、dependencyManagement中管理version、exclusion、scope等jar包属性,dependencies中负责实际引入不去关心以上所有信息,即使是单module工程也应该遵循改规则
4.2、使用tc组件的系统尽量使用TC POM去管理组件的jar包,规避组件之间的版本管理。外部公司相信也有本公司的类似的组件包管理pom,没有的话可以自己动手写一个。
4.3、使用第三方jar包,最好能够在pom文件中加上注释,并给个说明文档的链接
总结
林林总总说了这么多,希望能够对观者有些帮助,或者能够找到大家工作中遇到类似问题的解决思路。其实clean code是一个大家都知道都想要去争取的目标,希望会有些帮助给大家。
另外还有个问题,可能大家或多或少地会接触经历系统的重构,但是我想表达的是重构并不是要积累到一定阶段才发生的事情,重构应该在每时每刻,只要你心中有WTF!的感觉那么就开始你的重构吧,写出clean code,精简系统。开心coding 开心生活。
最后,给大家推荐两本书《代码整洁之道》、《改善既有代码的设计》,相信大家可能有所耳闻或者读过。分享下自己的读书经验,建议大家反复去看 反复去思考其中的奥义。也许是因为作者是外国人的原因 举的例子大多都是国外生活中的一些例子 可能会不是那么的形象贴切,可以结合自己看过的代码去理解去感悟。