一个需求在我们完成需求理解,方案评审后,最终需要落地到代码中
本文将从以下几个方面方面,介绍一些如何写出具有可读性,可维护性,整洁的代码
规范命名
计算科学中最难的两件事之一是命名(另一件是缓存失效)-- 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;
}
复制代码