编写可读代码,提高工作效率

本次分享是怎么做到“可读性”的

首先,正在进行的,说明下本文的可读性。
1.背景 根据今年形势996icu,加班加点的情况比较多。与其抱怨,不如改变。
从内因去改变:主题,编写可读代码,大大工作效率。
2.观点:代码是写给人看的,推荐一本书《编写可读代码的艺术》。
3.方法:以《编写可读代码的艺术》内容,结合实际项目代码,给出13条建议。
4.个性化建议。
5.项目代码举例。
6.总结,可读性的好处,解决的问题。

友情声明

本次分享中提到的想法实践,纯属个人见解,仅供参考,多多海涵。

一、主题:编写可读代码,提高工作效率

职业生涯难题,996icu,加班加点。
与其抱怨,不如改变。
外因,不可控。
内因,可控。
想想能为一起coding的同事做点什么?
《编写可读代码,提高工作效率》。

1.现状:加班加点

需求又变了?加班
工作量有点大,进度催的紧,先加班为敬
系统bug多,顾此失彼,心情忧伤。
接手同事代码,看不懂。没文档、没流程图,产品经理和测试,不停追问业务细节。
可测试。测试经常打听实现细节。
联调真费劲,队友真...

2.根本原因之一:代码可读性差

代码可读性差,不好维护,容易修改出问题。

3. 解决办法

抓住1个根本性问题:写出的代码,可读性强,能让人快速理解、轻松维护、容易扩展。

为什么说能?且听下文分解...

二、观点:代码是写给人看的

2013年读过一本书《编写可读代码的艺术》,推荐阅读。

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.xlsx
Maven打包到 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 sth
    var 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;
  }

  //bad
  private String getName(){
    return "My name is:"+Name+" !";
  }

2.5 变量命名的一致性

命名一致

   userName
   username
   name

类型一致

   varchar(32) comp_id, String compId
   int(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
     */
    @Override
    public Result export(Map params, HttpServletResponse resp) {
}

  /**
     * 导出列表excel
     */
    @Override
    public Result export(Map 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

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 resutMap = new HashMap<>();
            BillVo vo = new BillVo();
            UserInfoDto userInfo = getUserInfo();
            vo = getAuthBillVo(vo, userInfo);
            vo.setBillInAccStatus("0,1");
            vo.setStoredBillStatus("1");
            vo.setStartBillMonth("201901");//g过滤掉2018的未核账数据
            PageBean 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 a

int 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篇文章

用户输入网站地址:如“http://article.cn”;

浏览器解析网址到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. 2019年,账务系统,Web项目

例子1,定时任务,可读性更强

@Component
@Slf4j
public class TaskScheduler {
    @Resource
    private 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,相关代码,统一放在一起

 CrmApiRpc
 CrmInfoDelegate

例子3,核心业务,用1个类单独维护

 PaymentSplitService

例子4,用MybatisPlus框架,轻松coding少写代码

 AccConfigInfoController

2. 2017年,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.blog.csdn.net/article/details/12159019

2.读书笔记-编写可读代码的艺术[中]

https://fansunion.blog.csdn.net/article/details/12159345

3.读书笔记-编写可读代码的艺术[下]

https://fansunion.blog.csdn.net/article/details/12159431

4.《编写可读代码的艺术》

5.《重构-改善既有代码的设计》

个人观点:一次书写,人人阅读。 Write once,Read anyone。 Coding for fun, coding for my life。

你可能感兴趣的:(技术中台)