如何写出高可读性代码(一)

一个需求在我们完成需求理解,方案评审后,最终需要落地到代码中

本文将从以下几个方面方面,介绍一些如何写出具有可读性,可维护性,整洁的代码

规范命名

计算科学中最难的两件事之一是命名(另一件是缓存失效)-- Phil Karlton

命名是在编码过程中最常遇到的事,包括类名,字段名,方法名,参数名,局部变量名

准确

命名过于宽泛,不能精准描述,这是很多代码在命名上存在的严重问题,也是代码难以理解的根源所在

我们先看一段代码

public Integer processClassroom(List<ClassroomModuleDTO> classroomModuleDTOList) throws Exception {

    if (CollectionUtils.isEmpty(classroomModuleDTOList)) {

        return 0;

    }

    List<ClassroomModuleDO> classroomModuleDOList = new ArrayList<>(classroomModuleDTOList.size());

    for (ClassroomModuleDTO classroomModuleDTO : classroomModuleDTOList) {

        classroomModuleDOList.add(BeanCopy.dtoConvertDo(classroomModuleDTO, ClassroomModuleDO.class));

    }

    return this.classroomModuleDao.modifyClassroomModuleBatch(classroomModuleDOList);

}
复制代码

如果我问你,这段代码是做什么的?你需要通读整段代码,找出其中的逻辑,才能得出:是批量修改classroom

其实我们根本不用阅读完整个代码,最好的情况是,只用看看方法名,就知道方法的意图

将方法名改为:

public Integer BatchModifyClassroomModule(List<ClassroomModuleDTO> classroomModuleDTOList) throws Exception {
    // ...    
}
复制代码

将方法名从一个笼统的processXXX,修改为能表现其意图的命名,这样在外层看到接下来有一个方法调用时,立马就知道该方法的作用是批量修改classroom,就不用深入方法去阅读其怎么完成修改的细节。

实际上, 将代码提取成方法的目的之一就是对调用层屏蔽其实现细节,而一个精准的命名能帮助达到这一目的

符合业务

一个团队内每个开发可能对业务都有自己的理解,如果不加以规范,可能在代码库中对同一业务模型存在不同的命名,徒增代码理解成本

一个良好的实践是,将业务中的各种命名形成规范文档,放到新人入职文档中,这样从一开始就对齐命名规范。其次在code review中也可以进行纠正同步

无重复代码

写出重复代码是开发过程中常见的问题,当发现需要的功能正好和某处很相似时,看起来最简单的做法就是:将其复制过来,改几个地方,测试下,收工

但这样做会带来以下几个问题:

  • 仓库中代码量变大,增加理解成本,即每次阅读到这些代码时,都要从头到尾阅读,才能明白代码意图
  • 需求变动需要修改时,意味着所有复制粘贴的地方都要改,增加工作量
  • 修改的地方多,容易出现漏改,在code review中野不易被发现,造成线上问题

真正的做法是,将相似的部分提取成函数,然后在需要的地方调用这个函数,这样在上层我有兴趣,可以点进去看看方法具体实现,没兴趣就看方法名,了解其作用即可,不用每次阅读到这都要过一遍代码细节

避免长函数

当我们熟悉项目代码,或完成新需求需要了解某函数(甚至是自己写的)代码细节时,如果遇到连一个屏幕都放不下的长函数,一定会让人崩溃。因此我们要尽量避免写出长函数

多长算长函数?

我团队用的代码检测工具定义60行算长函数(go语言),根据语言的不同(python可以降低,java可以提高),或者团队的技术水平,长函数的定义可以适当调整。原则上来说是越低越好

什么情况下会产生长函数呢?

提高性能

确实,频繁的方法调用,会有一定的开销(保存参数,保存pc,开辟栈帧等)

但现在编译器通常有一些优化,例如go默认有方法内联优化。且业务程序执行的性能瓶颈通常在远程网络调用,例如rpc调用,访问db,而不在本地的方法调用

更重要的是,可维护性比性能优化要优先考虑,当性能不足以满足需要时,我们再来做相应的测量,找到焦点,进行特定的优化

未分离关注点

更可能的情况下,开发在平铺直叙地写代码,想到什么就写什么。这样会造成一个问题:

多个业务流程的细节都在一个方法中,增加理解成本

若一个长函数中有多个业务流程阶段,可以将其分别提取到单独的函数中,在该函数中调用各个阶段方法即可,这样根据方法名能一目了然地知道有哪些流程,而不用深入细节

拆解长函数数后,每个子函数上下文更短,其中的变量命名可以更短,因为变量都是在这个短小的上下文里,也就不会产生那么多的命名冲突,变量名当然就可以写短一些。上下文更短,理解的成本也会降低

大类

一个人理解的东西是有限的,没有人能同时面对所有细节

各种编程语言都有按模块,或包划分代码的功能,当把各种类按照模块划分后,人们面对的就不再是细节,而是模块,模块的数量显然会比细节数量少,人们的理解成本就降低了

同理,具体到每一个类(或结构体)上,若其中的字段太多,超过人们容易理解的范畴,一些开发上的疏忽就可能出现

解决大类的方法就是将其拆分为多个小类

举个例子,一个User类有以下字段

public class User{

    private Long id;

    private Long userId;

    private String name;

    private String realName;

    private String nick;

    private Integer sex;

    private String email;

    private String mobile;

    private String smallPhoto;

    private String bigPhoto;

    private String registerIp;

    private String wxOpenId;

    private String facebookId;

    private String lastLoginIp;

    private Date lastLoginTime;

}
复制代码

User类符合一个大类的特征,拥有大量字段。我们看看可否进行拆分

前面的字段确实是用户的基础信息,包含name,sex,email等

而到了lastLoginIp,lastLoginTime,这两个就不算用户基础信息了,属于登录信息

从需求上看,基本信息是那种一旦确定就不怎么会改变的内容,而登录则是每次登录都会更新

这样造成的结果就是,任何一次登录,都会让这个类反复修改,违反了单一职责原则

因此我们把这些信息放到一个新类LoginInfo中

除此之外,User类还可以按照模块进一步细分,将email,mobile字段提取为Contact类,将wxOpenId,facebookId字段提取为ThirdInfo类

拆分后的类如下所示:

public class User{

    private Long id;

    private Long userId;

    private String name;

    private String realName;

    private String nick;

    private Integer sex;

    private String smallPhoto;

    private String bigPhoto;

    private String registerIp;

    

}



class Contact {

    private String email;

    private String mobile;

}



class ThirdInfo {

    private String wxOpenId;

    private String facebookId;

}



class LoginInfo {

    private String lastLoginIp;

    private Date lastLoginTime;

}
复制代码

猜你喜欢

转载自juejin.im/post/7053823133366616100