本次分享是怎么做到“可读性”的
首先,正在进行的,说明下本文的可读性。
1.背景 根据今年形势996icu,加班加点的情况比较多。与其抱怨,不如改变。
从内因去改变:主题,编写可读代码,大大工作效率。
2.观点:代码是写给人看的,推荐一本书《编写可读代码的艺术》。
3.方法:以《编写可读代码的艺术》内容,结合实际项目代码,给出13条建议。
4.个性化建议。
5.项目代码举例。
6.总结,可读性的好处,解决的问题。
友情声明
本次分享中提到的想法实践,纯属个人见解,仅供参考,多多海涵。
一、主题:编写可读代码,提高工作效率
职业生涯难题,996icu,加班加点。
与其抱怨,不如改变。
外因,不可控。
内因,可控。
想想能为一起coding的同事做点什么?
《编写可读代码,提高工作效率》。
1.现状:加班加点
需求又变了?加班
工作量有点大,进度催的紧,先加班为敬
系统bug多,顾此失彼,心情忧伤。
接手同事代码,看不懂。没文档、没流程图,产品经理和测试,不停追问业务细节。
可测试。测试经常打听实现细节。
联调真费劲,队友真...
2.根本原因之一:代码可读性差
代码可读性差,不好维护,容易修改出问题。
3. 解决办法
抓住1个根本性问题:写出的代码,可读性强,能让人快速理解、轻松维护、容易扩展。
为什么说能?且听下文分解...
二、观点:代码是写给人看的
读过一本书《编写可读代码的艺术》,推荐阅读。
1.观点
程序员之间的互相尊重体现在他所写的代码中,他们对工作的尊重也体现在那里。
代码最重要的读者不是编译器,解释器或电脑,而是人。
写出的代码能让人快速理解、轻松维护、容易扩展的程序员才是专业的程序员。
2 可读性怎么定义?
可读性基本定理:代码的写法,应当使别人理解它所需要的时间最小化。
"别人"应当指所有阅读你的代码的人,包括同事,也包括6个月后的你自己!
2.1 示例1:
增加变量让代码更可读
if(name != null && name != ""){}if(StringUtils.isNotEmpty(name)){}boolean isNotEmpty = StringUtils.isNotEmpty(name);if(isNotEmpty){}boolean isNotEmpty = StringUtils.isNotEmpty(name);if(isNotEmpty && name != "admin"){}
2.2 示例2:
需求变动:开始只有1个规则,后来变为2个规则
boolean ruleOne =calcRuleOne();boolean roleTwo = calcRuleTwo();if(ruleOne && ruleTwo){}else if(){}else if(){}else{}
2.3 示例3
如果不认真写代码,出了bug,让你怀疑人生。
@RestController@RequestMapping("manage/dictionary")public class DictionaryController extends BaseController{//获得数据字典@RequestMapping("/getDictionary")private String getDictionary(Dictionary dictionary){// code}//收款人列表@RequestMapping("/payeeList")public String payeeList(){// code}}
2.3 示例4,歧义的目录
src/main/resources/template/abc.xlsx(Maven打包到 classes/template/abc.xlsx)Generator.class.getClassLoader().getResourceAsStream("template/abc.xlsx");
src/main/template/abc.xlsxMaven打包到 classes/abc.xlsx)Generator.class.getClassLoader().getResourceAsStream("abc.xlsx");
三、方法:编写可读代码,13条建议
第一部分 表面层次的改进
1.把信息装到名字里
1.1选择专业的词
getPage(url); 是从缓存获取页面,还是实时从互联网上获取呢?
根据url,获得1页内容?获得1个变量。
更专业的词:fetchPageFromCache,downloadPage,getPage。
1.2避免象tmp这样泛泛的名字
String tmp =user.name();tmp += " "+user.email();
用userInfo这样的名字更具有描述性。
建议:tmp这个名字只应用于短期存在且临时性为其主要存在因素的变量。
1.3 用具体的名字代替抽象的名字
serverCanStart:检测服务是否可以监听某个给定的TCP/IP端口。
更好的名字:
canLinstenOnPort:这个名字直接地描述了这个方法要做什么事情。
1.4为名字附带更多信息
var start = new Date().getTime();//do sthvar end = new Date().getTime();var costTime = (start-end)/1000;(时间的单位是秒s,还是毫秒ms?)costTimeMs?
1.5名字应该有多长
int d;int days;int daysSinceLastUpdate;
在小的作用域可以使用短的名字,大的作用域使用长的名字。看看当前上下文是否有足够的信息。
1.6利用名字的格式来传递含义
//常量名和类名的取名方式不一样
private String userBtn;public static final int MAX_NUMBER= 100;public class Number{}
2.不会误解的名字
2.1容易产生误解的例子
//挑出?减掉?allPersons.filter(“age>100”);
2.2 推荐用first和last来表示包含的范围
推荐用begin和end来表示包含/排除范围
String str ="abcd";str.substring1(int first,int last);str.substring2(int bigin,int end);
2.3 给布尔值命名
public boolean addUser(){boolean flag= true;return flag;}
把flag换成addSucceed
2.4与使用者的期望相匹配
private String name;//很多程序都习惯了把以get开始的方法当作“轻量级访问器”这样的用法//它只是简单地返回一个内部成员变量。private String getName(){return name;}//badprivate String getName(){return "My name is:"+Name+" !";}
2.5 变量命名的一致性
命名一致
userNameusernamename
类型一致
varchar(32) comp_id, String compIdint(11) comp_id , Integer compId;
3.审美
3.1把声明按块组织起来
//get/query/find/select 查询类方法 (高频方法)//add 增加类方法//update 修改类方法//delete 删除类方法
3.2把代码分成“段落”
String name;updateName="";String email;sendEmail();String address;saveAddress();
3.3个人风格与一致性
class Name{}class Name{}
一致的风格比“正确”的风格更重要。
4.该写什么样的注释
4.1什么不需要注释
//用户模块public class UserService{}
建议:不要为那些能从代码本身快速推断的事实写注释。
boolean isEmpty = StringUtils.isEmpty(name);if(isEmpty){}
没用的注释/*** 导出列表excel** @param params* @param resp* @return*/@Overridepublic Result export(Map<String, Object> params, HttpServletResponse resp) {}/*** 导出列表excel*/@Overridepublic Result export(Map<String, Object> params, HttpServletResponse resp) {}
4.2记录你的思想
4.2.1 加入“导演评论”
//准确率可以达到99%,没有必要达到100%
getValue();
4.2.2 为代码中的瑕疵写注释
//冒泡排序不够快
bubbleSort();
4.2.3 给常量加注释
//人的最大年龄
public static final int MAX_AGE=150;
4.3 站在读者的角度
4.3.1公布可能的陷阱
//调用外部服务来发送邮件。(1分钟之后超时)
sendEmail();
4.3.2 总结性注释
//求和
int[] array = {1,2,3};for(int index=0;index<array.length;index++){sum += array[index];}
4.4 精确地描述函数的行为
//返回文件的行数//计算换行符(\n)的个数int countLines(String fileName);
项目代码举例
@Slf4j@RestController@RequestMapping(value = "/api/bill")public class BillController extends BaseController {/*** 获取账单列表和回款列表未处理的记录总数*/@RequestMapping(value = "/undealwithcount")public Result getUndealwithCount() {try {Integer billResultCount = 0;Integer refundResultCount = 0;Map<String, Integer> resutMap = new HashMap<>();BillVo vo = new BillVo();UserInfoDto userInfo = getUserInfo();vo = getAuthBillVo(vo, userInfo);vo.setBillInAccStatus("0,1");vo.setStoredBillStatus("1");vo.setStartBillMonth("01");//g过滤掉的未核账数据PageBean<AccountStatement> billDataPage = billService.getBillDataPageByRoleId(vo);long totalRecord = billDataPage.getTotalRecord();billResultCount = (int) totalRecord;log.info("bicontroller getUndealwithCount query refund from bi param userId={}", userInfo.getUserId());String unSplitCountUrl = unSplitCounturl + "?casUserId=" + userInfo.getUserId();log.info("bicontroller getUndealwithCount query refund from bi param unSplitCountUrl={}", unSplitCountUrl);String resp = httpClientUtil.get(unSplitCountUrl);log.info("bicontroller getUndealwithCount query refund from bi result resp={}", resp);if (null != resp && StringUtils.isNotBlank(resp)) {JSONObject jsonObject = JSONObject.parseObject(resp);String code = jsonObject.getString("code");Integer data = jsonObject.getInteger("data");String message = jsonObject.getString("message");if ("000000".equals(code)) {refundResultCount = data;} else {log.info("查询bi系统回款列表未拆分记录数异常message={}", message);}}resutMap.put("billList", billResultCount);resutMap.put("refundList", refundResultCount);return ResultUtils.success(resutMap);} catch (Exception e) {log.error("获取账单未核算记录总数失败,异常信息={}.", e);e.printStackTrace();return ResultUtils.error(ResultEnums.QUERY_FAIL_ERROR);}}}```
一个方法,8处可改进
变量作用域过大
vo被改变了吗?
BillController里出现了bicontroller
billList和billResultCount
异常时,用error
log记录error有问题
如果正确,log打印了异常,还需要"e.printStackTrace()"
log.error("获取账单未核算记录总数失败,异常信息={}.", e);
e.printStackTrace();
代码重复
null != resp && StringUtils.isNotBlank(resp)
提取子方法,1个方法解决1个问题
String resp = httpClientUtil.get(unSplitCountUrl);
第2部分 简化循环和逻辑
5.把控制流变得易读
关键思想:把条件、循环以及其它对控制流的改变做得越“自然”越好。
运用一种方式使读者不用停下来重读你的代码。
5.1条件语句中参数的顺序
if(age >20){}比if(20 < age){}
更易读。
if(name == null){}比if(null == name){}
在中文和英文等自然语言中(“如果你的年龄大于20”)更常见,更符合一般用法。
即比较的左侧,它的值倾向于不断变化,比较的右侧,它的值倾向于稳定。
5.2 if/else语句块的顺序
if(a== b){//case one}else{//case two}
也可以写成
if(a != b){//case one}else{//case two}
之前你可能没想太多,但在有些情况下有理由相信其中一种顺序比另一种好:
a.首先处理正逻辑而不是负逻辑的情况。例如,if(debug)而不是if(!debug)。
b.先处理简单的情况。这种方式可能还会使得if和else在屏幕之内都可见,这很好。
c.先处理有趣的或者是可疑的情况。
下面所示是负逻辑更简单并且更有趣的一种情况,那么会先处理它
if (not the same username){//case one}else{//case two}
5.3三目运算符
它对于可读性的影响是富有争议的。
拥护者认为这种方式可以只写一行而不用写成多行,
反对者则说这可能会造成阅读的混乱而且很难用调试器来调试。
关键思想:相对于追求最小化代码行数,一个更好的度量方法是最小化人们理解它所需的时间。
建议:默认情况下都用if/else。
三目运算符?:只有在最简单的情况下使用。
5.4避免do/while循环
do{}while(condition);
do/while循环的奇怪之处是一个代码块是否会执行,是由其后的一个条件决定的。
通常来讲,逻辑条件应该出现在它们“保护”的代码之前,这是if,while和for语句的工作方式。
因为你通常会从前向后来读代码,这使得do/while循环有点不自然了。
5.5从函数中提前返回
public boolean contains(String str,String substr){if(str==null || substr==null){return false;}if(substr.equals("")){return true;}...}
5.6最小化嵌套
if(userResult==SUCCESS){if(permissionResult != SUCCESS){reply.writeErrors("error reading permission");reply.done();return;}reply.writeErrors("");}else{reply.writeErrors(userResult);}reply.done();
可以通过提前返回,来减少嵌套。
6.拆分超长的表达式
6.1用做解释的变量
if(line.split(",")[0].name=="root"){}增加一个解释变量String username = line.split(",")[0].name;if(name=="root"){}
6.2总结变量
即使一个表达式不需要变量(因为你可以看出它的含义),把它装入一个新变量中仍然有用。
我们把它叫做总结变量,因为它的目的是用一个短很多的名字来代替一大块代码,
这个名字会更容易思管理和思考。
if(request.user.id == document.user.id){//user can edit this document}if(request.user.id != document.user.id){//document is read only}
这里的表达式“request.user.id==document.user.id”看上去可能并不长,
但它包含5个变量,所以需要多花点时间来想一想如何处理它。
这段代码中的主要概念是:“该用户拥有此文档吗?”
这个概念可以通过增加一个总结变量来表达得更清楚。
final boolean userOwnDocument = (request.user.id==document.user.id);if(userOwnDocument){...}if(!userOwnDocument){...}
7.变量与可读性
关于变量的3个问题
a.变量越多,就越难全部跟踪它们的动向。
b.变量的作用域越大,就需要跟踪它的动向更久。
c.变量改变得越频繁,就越难以跟踪它的当前值。
7.1减少变量
没有价值的临时变量
now = datetime.time();rootMessage.lastVisitTime=now;
减少控制流变量
boolean done=false;if(condition && !done){if(...){done=true;continue;}}
可以改为
if(condition){if(...){break;}}
7.2缩小变量的作用域
把定义向下移int a=0;int b=0;int c=0;//handle a//handle b//handle c改为int a=0;//handle aint b=0//handle b
全局变量改为局部变量。
7.3 只写一次的变量更好
"1"表示什么意思?
vo.setStoredBillStatus("1");public static final int MAX_AGE=140;
常量、枚举,可能更能表达变量的含义
public enum CompLevelEnum {SME(4, "小客户"),GENERAL_CUSTOMER(3, "一般客户"),AREA_KEY_CUSTOMER(2, "区域级重点客户"),COMP_KEY_CUSTOMER(1, "公司级重点客户");private Integer code;private String message;CompLevelEnum(Integer code, String message) {this.code = code;this.message = message;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}}
第三部分 重新组织代码
8. 三种组织代码的方法
a.抽取出那些与程序主要目的“不相关的子问题”。
b.重新组织代码,使它一次只做一件事情。
c.先用自然语言描述代码,然后用这个描述来帮助你找到更整洁的解决方案。
9.抽取不相关的子问题
本章的建议是“积极地发现并抽取不相关的自逻辑”,我们是指:
a.看看某个函数或代码块,问问你自己,这段代码高层次的目标是什么?
b.对于每一行代码,问一下:它是直接未来目标而工作吗?这段代码高层次的目标是什么呢?
c.如果足够的行数在解决不相关的子问题,抽象代码到独立的函数中。
介绍性的例子
int[] array = {2,4,1,3}; 求最大值和最小值。 void method(){ //排序函数,这就是1个子问题 //取第1个和最后1个 }
10.纯工具代码
文件操作,邮件发送等。
创建大量通用代码
通用代码,它完全地从项目的其它部分解耦出来。这样的代码容易开发,容易测试,并且容易理解。SQL数据库、JavaScript库、XML库等。
项目专有的功能
把名字转换成1个URL,这类项目特有的功能,也是可以提取出来的。
11.一次只做一件事情
同时在做几件事的代码很难理解。
一个代码块可能初始化对象,清除数据,解析输入,然后应用业务逻辑,所有这些都同时进行。
如果所有这些代码都纠缠在一起,对于每个"任务"都很难靠其自身来帮你理解它从哪里开始,到哪里结束。
12. 把想法变成代码
当你把一件复杂的事向别人解释时,那些小细节很容易就会让他们迷惑。
把一个想法用“自然语言”解释是个很有价值的能力,因为这样其它知识没有你这么渊博的人才可以理解它。 这需要把一个想法精炼成最重要的概念。
这样做,不仅帮助他人理解,而且也帮助你自己把这个想法想得更清楚。
一个示例:用户在浏览器访问1篇文章
用户输入网站地址:如“”;
浏览器解析网址到IP,如122.96.184.84;
浏览器建立和该IP的Socket;
浏览器与该主机通信,取得网页;
显示网页内容。
13.少写代码
知道什么时候不写代码,可能对于一个程序员来讲是他所要学习的最重要的技巧。
你所写的每一行代码都是需要测试和维护的。
通过重用库或者减少功能,你可以节省时间并且让你的代码保持精简节约。
最好读的代码就是没有代码。
13.1保持小代码库。
创建“工具”代码减少重复代码;减少无用代码或者没有用的功能; 在一个成熟的库中,每一行代码都代表大量的设计、调试、重构、文档、优化和调试。
Collections,Lang,BeanUtils,Compress
13.2 别费神实现那个功能–你不会需要它。
很多功能没有完成,或者没有用,也可能是让程序更复杂。
一个功能,不是只有开发,还有测试,最后还有维护和升级。
13.3 质疑和拆分你的需求。
不是所有的程序都需要运行的快,100%准确,并且能处理所有的输入。 如果你真的仔细检查你的需求,有时你可以把它削减成一个简单的问题。
四、6点个性化建议
1、不错的建议
1.1约定优于配置
user_name,userName,UserMapper,UserService,UserController
1.2只写必要的注释,业务复杂的地方写注释
1.3削减代码行数
2、有争议的建议,个人特殊习惯
2.1 Service只要实现类,不要接口。
2.2 数据库字段,采用java驼峰命名,减少映射。
2.3 慎用设计模式。
五、代码举例:Talk is cheap, Show me the Code
1. ,账务系统,Web项目
例子1,定时任务,可读性更强
@Component@Slf4jpublic class TaskScheduler {@Resourceprivate CrmApiRpc crmApiRpc;/*** 1小时1次*/@Scheduled(initialDelay = 30*60*1000, fixedRate = 1* 60 * 60 * 1000)public void task1() {log.info("-----task start-----");try {doTask();} catch (Exception e) {log.error("task error", e);}log.info("-----task end-----");}}/*** 1小时1次*/@Scheduled(cron = "0 */1 * * *")public void task2(){}
例子2,相关代码,统一放在一起
CrmApiRpcCrmInfoDelegate
例子3,核心业务,用1个类单独维护
PaymentSplitService
例子4,用MybatisPlus框架,轻松coding少写代码
AccConfigInfoController
2. ,Mybatis代码生成器,工具项目
可借鉴的点:流程清晰 生成器入口
3.1初始化配置
3.2根据配置生成代码
3.2.1获得数据库连接
3.2.2得到所有表名
3.2.3循环生成每个表对应的模版
1).根据数据库连接和数据库表名,构造模版的数据模型2).将Java模型转换成Map格式3).生成4个标准文件(读取模版,根据Map,渲染,保存)a. GeneratorTool.generateModel(generatorModel);b. GeneratorTool.generateBean(generatorModel);c. GeneratorTool.generateMapperJava(generatorModel);d. GeneratorTool.generateMapperXml(generatorModel);
3.3自动打开生成文件的目录
六、总结
1.可读代码的手段
命名精确,望文知义、单一职责、及时重构、流程清晰、可测试
2.可读代码的好处
降低复杂度-读懂代码花费的时间少、方便修改和维护、方便交接(代码)、bug少、方便测试、方便复用和重构、与产品经理测试等非写代码的人交流。
3.结论
编写可读代码,提高工作效率。
七、QA
八、参考资料
1.读书笔记-编写可读代码的艺术[上]
https://fansunion./article/details/12159019
2.读书笔记-编写可读代码的艺术[中]
https://fansunion./article/details/12159345
3.读书笔记-编写可读代码的艺术[下]
https://fansunion./article/details/12159431
4.《编写可读代码的艺术》
5.《重构-改善既有代码的设计》
个人观点:一次书写,人人阅读。 Write once,Read anyone。 Coding for fun, coding for my life。
如果觉得《编写可读代码 提高工作效率》对你有帮助,请点赞、收藏,并留下你的观点哦!