本文作者:陈昌毅,花名常意,高德地图技术专家,2018年加入阿里巴巴,一直从事地图数据采集的相关工作。
原文地址:Java 函数优雅之道
导读
随着软件项目代码的日积月累,系统维护成本变得越来越高,是所有软件团队面临的共同问题。持续地优化代码,提高代码的质量,是提升系统生命力的有效手段之一。软件系统思维有句话“Less coding, more thinking(少编码、多思考)”,也有这么一句俚语“Think more, code less(思考越多,编码越少)”。所以,我们在编码中多思考多总结,努力提升自己的编码水平,才能编写出更优雅、更高质、更高效的代码。
本文总结了一套与 Java 函数相关的编码规则,旨在给广大Java程序员一些编码建议,有助于大家编写出更优雅、更高质、更高效的代码。这套编码规则,通过在高德采集部门的实践,已经取得了不错的成效。
使用通用工具函数
案例一
现象描述:
不完善的写法:
1 | thisName != null && thisName.equals(name); |
更完善的写法:
1 | (thisName == name) || (thisName != null && thisName.equals(name)); |
建议方案:
1 | Objects.equals(name, thisName); |
案例二
现象描述:
1 | !(list == null || list.isEmpty()); |
建议方案:
1 | import org.apache.commons.collections4.CollectionUtils; |
主要收益
- 函数式编程,业务代码减少,逻辑一目了然;
- 通用工具函数,逻辑考虑周全,出问题概率低。
拆分超大函数
当一个函数超过80行后,就属于超大函数,需要进行拆分。
案例一
每一个代码块必然有一个注释,用于解释这个代码块的功能。如果代码块前方有一行注释,就是在提醒你——可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。如果函数有一个描述恰当的名字,就不需要去看内部代码究竟是如何实现的。
现象描述:
1 | // 每日生活函数 |
建议方案:
1 | // 每日生活函数 |
案例二:每一个循环体都可以封装为一个函数。
现象描述:
1 | // 生活函数 |
建议方案:
1 | // 生活函数 |
案例三:每一个条件体都可以封装为一个函数。
现象描述:
1 | // 外出函数 |
建议方案:
1 | // 外出函数 |
主要收益
- 函数越短小精悍,功能就越单一,往往生命周期较长;
- 一个函数越长,就越不容易理解和维护,维护人员不敢轻易修改;
- 在过长函数中,往往含有难以发现的重复代码。
同一函数内代码块级别尽量一致
案例一
现象描述:
1 | // 每日生活函数 |
很明显,睡觉这块代码块,跟 eat(吃饭)和 code(编码)不在同一级别上,显得比较突兀。如果把写代码比作写文章,eat(吃饭)和code(编码)是段落大意,而睡觉这块代码块属于一个详细段落。而在 liveDaily(每日生活)这个函数上,只需要写出主要流程(段落大意)即可。
建议方案:
1 | public void liveDaily() { |
主要收益
- 函数调用表明用途,函数实现表达逻辑,层次分明便于理解;
- 不用层次的代码块放在一个函数中,容易让人觉得代码头重脚轻。
封装相同功能代码为函数
案例一:封装相同代码为函数
现象描述:
1 | // 禁用用户函数 |
建议方案:
1 | // 禁用用户函数 |
案例二:封装相似代码为函数
封装相似代码为函数,差异性通过函数参数控制。
现象描述:
1 | // 通过工单函数 |
建议方案:
1 | // 通过工单函数 |
主要收益
- 封装公共函数,减少代码行数,提高代码质量;
- 封装公共函数,使业务代码更精炼,可读性可维护性更强。
封装获取参数值函数
案例一
现象描述:
1 | // 是否通过函数 |
建议方案:
1 | // 是否通过函数 |
主要收益
- 把获取参数值从业务函数中独立,使业务逻辑更清晰;
- 封装的获取参数值为独立函数,可以在代码中重复使用。
通过接口参数化封装相同逻辑
案例一
现象描述:
1 | // 发送审核员结算数据函数 |
建议方案:
1 | // 发送审核员结算数据函数 |
主要收益
- 把核心逻辑从各个业务函数中抽析,使业务代码更清晰更易维护;
- 避免重复性代码多次编写,精简重复函数越多收益越大。
减少函数代码层级
如果要使函数优美,建议函数代码层级在1-4之间,过多的缩进会让函数难以阅读。
案例一:利用return提前返回函数
现象描述:
1 | // 获取用户余额函数 |
建议方案:
1 | // 获取用户余额函数 |
案例二:利用continue提前结束循环
现象描述:
1 | // 获取合计余额函数 |
建议方案:
1 | // 获取合计余额函数 |
特殊说明
其它方式:在循环体中,先调用案例1的函数 getUserBalance (获取用户余额),再进行对余额进行累加。
在循环体中,建议最多使用一次 continue 。如果需要有使用多次 continue 的需求,建议把循环体封装为一个函数。
案例三:利用条件表达式函数减少层级
请参考下一章的”案例2: 把复杂条件表达式封装为函数”
主要收益
- 代码层级减少,代码缩进减少;
- 模块划分清晰,方便阅读维护。
封装条件表达式函数
案例一:把简单条件表达式封装为函数
现象描述:
1 | // 获取门票价格函数 |
建议方案:
1 | // 获取门票价格函数 |
案例二:把复杂条件表达式封装为函数
现象描述:
1 | // 获取土豪用户列表 |
建议方案:
1 | // 获取土豪用户列表 |
以上代码也可以用采用流式(Stream)编程的过滤来实现。
主要收益
- 把条件表达式从业务函数中独立,使业务逻辑更清晰;
- 封装的条件表达式为独立函数,可以在代码中重复使用。
尽量避免不必要的空指针判断
本章只适用于项目内部代码,并且是自己了解的代码,才能够尽量避免不必要的空指针判断。对于第三方中间件和系统接口,必须做好空指针判断,以保证代码的健壮性。
案例一: 调用函数保证参数不为空,被调用函数尽量避免不必要的空指针判断
现象描述:
1 | // 创建用户信息 |
建议方案:
1 | // 创建用户信息 |
案例二: 被调用函数保证返回不为空,调用函数尽量避免不必要的空指针判断
现象描述:
1 | // 保存用户函数 |
建议方案:
1 | // 保存用户函数 |
案例三:赋值逻辑保证列表数据项不为空,处理逻辑尽量避免不必要的空指针判断
现象描述:
1 | // 查询用户列表 |
建议方案:
1 | // 查询用户列表 |
案例四:MyBatis查询函数返回列表和数据项不为空,可以不用空指针判断
MyBatis 是一款优秀的持久层框架,是在项目中使用的最广泛的数据库中间件之一。通过对 MyBatis 源码进行分析,查询函数返回的列表和数据项都不为空,在代码中可以不用进行空指针判断。
现象描述:
这种写法没有问题,只是过于保守了。
1 | // 查询用户函数 |
建议方案:
1 | // 查询用户函数 |
主要收益
- 避免不必要的空指针判断,精简业务代码处理逻辑,提高业务代码运行效率;
- 这些不必要的空指针判断,基本属于永远不执行的 Death 代码,删除有助于代码维护。
内部函数参数尽量使用基础类型
案例一:内部函数参数尽量使用基础类型
现象描述:
1 | // 调用代码 |
建议方案:
1 | // 调用代码 |
案例二:内部函数返回值尽量使用基础类型
现象描述:
1 | // 获取订单总额函数 |
建议方案:
1 | // 获取订单总额函数 |
此处只是举例说明这种现象,更好的方式是采用流式(Stream)编程。
主要收益
- 内部函数尽量使用基础类型,避免了隐式封装类型的打包和拆包;
- 内部函数参数使用基础类型,用语法上避免了内部函数的参数空指针判断;
- 内部函数返回值使用基础类型,用语法上避免了调用函数的返回值空指针判断。
尽量避免返回的数组和列表为 null
案例一:尽量避免返回的数组为 null ,引起不必要的空指针判断
现象描述:
1 | // 调用代码 |
建议方案:
1 | // 调用代码 |
案例二:尽量避免返回的列表为 null ,引起不必要的空指针判断
现象描述:
1 | // 调用代码 |
建议方案:
1 | // 调用代码 |
主要收益
- 保证返回的数组和列表不为 null , 避免调用函数的空指针判断。
封装函数传入参数
案例一:当传入参数过多时,应封装为参数类
Java 规范不允许函数参数太多,不便于维护也不便于扩展。
现象描述:
1 | // 修改用户函数 |
建议方案:
1 | // 修改用户函数 |
案例二: 当传入成组参数时,应封装为参数类
既然参数成组出现,就需要封装一个类去描述这种现象。
现象描述:
1 | // 获取距离函数 |
建议方案:
1 | // 获取距离函数 |
主要收益
- 封装过多函数参数为类,使函数更便于扩展和维护;
- 封装成组函数参数为类,使业务概念更明确更清晰。
尽量用函数替换匿名内部类的实现
在匿名内部类(包括Lambda表达式)中可以直接访问外部类的成员,包括类的成员变量、函数的内部变量。正因为可以随意访问外部变量,所以会导致代码边界不清晰。
首先推荐用 Lambda 表达式简化匿名内部类,其次推荐用函数替换复杂的 Lambda 表达式的实现。
案例一:尽量用函数替换匿名内部类(包括Lambda表达式)的实现
现象描述:
1 | // 发送结算数据 |
建议方案:
1 | // 发送结算数据 |
其实,还有一个更简单的办法。在调用函数 sendWorkerSettleData(发送作业员结算数据)之前计算开始日期、结束日期,就直接可以用函数auditTaskDAO.statCheckerSettleData(beginDate, endDate)代替匿名内部类实现。
案例二:拆分复杂匿名内部类实现接口为多个函数类接口
如果一个匿名内部类实现的接口几个函数间关联性不大,可以把这个接口拆分为几个函数式接口,便于使用 Lambda 表达式。
现象描述:
1 | // 清除过期数据 |
建议方案:
1 | // 清除过期数据 |
主要收益
- 定义函数并指定参数,明确规定了匿名内部类的代码边界;
- 利用 Lambda 表达式简化匿名内部类实现,使代码更简洁。
利用 return 精简不必要的代码
案例一:删除不必要的 if
现象描述:
1 | // 是否通过函数 |
建议方案:
1 | // 是否通过函数 |
案例二:删除不必要的 else
现象描述:
1 | // 结算工资函数 |
建议方案:
1 | // 结算工资函数 |
案例三:删除不必要的变量
现象描述:
1 | // 查询用户函数 |
建议方案:
1 | // 查询用户函数 |
主要收益
- 精简不必要的代码,让代码看起来更清爽。
利用临时变量优化代码
在一些代码中,经常会看到 a.getB().getC()…getN() 的写法,姑且叫做“函数的级联调用”,代码健壮性和可读性太差。建议:杜绝函数的级联调用,利用临时变量进行拆分,并做好对象空指针检查。
案例一:利用临时变量厘清逻辑
现象描述:
1 | // 是否土豪用户函数 |
这是精简代码控的最爱,但是可读性实在太差。
建议方案:
1 | // 是否土豪用户函数 |
这个方案,增加了代码行数,但是逻辑更清晰。
有时候,当代码的精简性和可读性发生冲突时,个人更偏向于保留代码的可读性。
案例二:利用临时变量精简代码
现象描述:
1 | // 构建用户函数 |
这么写,大约是为了节约一个临时变量吧。
建议方案:
1 | // 构建用户函数 |
主要收益
- 利用临时变量厘清逻辑,显得业务逻辑更清晰;
- 利用临时变量精简代码,看变量名称即知其义,减少了大量无用代码;
- 如果获取函数比较复杂耗时,利用临时变量可以提高运行效率;
- 利用临时变量避免函数的级联调用,可有效预防空指针异常。
仅保留函数需要的参数
案例一:删除多余的参数
现象描述:
1 | // 修改用户状态函数 |
其中,unused 参数是无用参数。
建议方案:
1 | // 修改用户状态函数 |
案例二:用属性取代对象
现象描述:
1 | // 删除用户函数 |
建议方案:
1 | // 删除用户函数 |
建议方案:
调用函数时,参数对象不需要专门构建,而函数使用其属性超过3个,可以不必使用该规则。
主要收益
- 仅保留函数需要的参数,明确了调用时需要赋值的参数,避免了调用时还要去构造些无用参数。