@TOC
1. 导读 昨天在技术交流群,有位读者吐槽刚刚接手的代码(文章标题),代码全篇无格式,无注释,多个嵌套不打括号,函数命名不规范,代码实在看不下去。随着软件项目代码的日积月累,系统维护成本变得越来越高,代码质量是所有软件团队面临的共同问题。
持续地优化代码,提高代码的质量,是提升系统生命力的有效手段之一。软件系统思维有句话“Less coding, more thinking(少编码、多思考)”,也有这么一句俚语“Think more, code less(思考越多,编码越少)”。所以,我们在编码中多思考多总结,努力提升自己的编码水平,才能编写出更优雅、更高质、更高效的代码。
刚刚接触项目或者实习阶段,写出的代码质量差难免被吐槽,本文便总结了一套与Java函数相关的编码规则,旨在给广大Java程序员一些编码建议,有助于大家编写出更优雅、更高质、更高效的代码。这套编码规则,通过在高德采集部门的实践,已经取得了不错的成效。
2.使用通用工具函数 2.1 案例一 现象描述:
不完善的写法:
1 thisName != null && thisName.equals(name);
更完善的写法:
1 (thisName == name) || (thisName != null && thisName.equals(name));
建议方案
:
1 Objects.equals(name, thisName);
2.2 案例二 现象描述:
1 !(list == null || list.isEmpty());
建议方案
:
1 2 import org.apache.commons.collections4.CollectionUtils;CollectionUtils.isNotEmpty(list);
2.3 主要收益
函数式编程,业务代码减少,逻辑一目了然;
通用工具函数,逻辑考虑周全,出问题概率低
3. 拆分超大函数 当一个函数超过80行后,就属于超大函数,需要进行拆分。
3.1 案例一:每一个代码块都可以封装为一个函 每一个代码块必然有一个注释,用于解释这个代码块的功能。
如果代码块前方有一行注释,就是在提醒你——可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。如果函数有一个描述恰当的名字,就不需要去看内部代码究竟是如何实现的。
现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 public void liveDaily () { }
建议方案
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public void liveDaily () { eat(); code(); sleep(); } private void eat () { } private void code () { } private void sleep () { }
3.2 案例二:每一个循环体都可以封装为一个函 现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void live () { while (isAlive) { eat(); code(); sleep(); } }
建议方案
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void live () { while (isAlive) { liveDaily(); } } private void liveDaily () { eat(); code(); sleep(); }
3.3 案例三:每一个条件体都可以封装为一个函 现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 public void goOut () { if (isWeekday()) { } else { } }
建议方案
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void goOut () { if (isWeekday()) { play(); } else { work(); } } private void play () { } private void work () { }
3.4 主要收益 函数越短小精悍,功能就越单一,往往生命周期较长;
一个函数越长,就越不容易理解和维护,维护人员不敢轻易修改;
在过长函数中,往往含有难以发现的重复代码。
4. 同一函数内代码块级别尽量一致 4.1 案例一 现象描述:
1 2 3 4 5 6 7 8 9 10 11 public void liveDaily () { eat(); code(); }
很明显,睡觉这块代码块,跟eat(吃饭)和code(编码)不在同一级别上,显得比较突兀。如果把写代码比作写文章,eat(吃饭)和code(编码)是段落大意,而睡觉这块代码块属于一个详细段落。而在liveDaily(每日生活)这个函数上,只需要写出主要流程(段落大意)即可。
建议方案
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void liveDaily () { eat(); code(); sleep(); } private void sleep () { }
4.2 主要收益
5.封装相同功能代码为函数 5.1 案例一:封装相同代码为函数 现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void disableUser () { List<Long> userIdList = queryBlackUser(); for (Long userId : userIdList) { User userUpdate = new User(); userUpdate.setId(userId); userUpdate.setEnable(Boolean.FALSE); userDAO.update(userUpdate); } userIdList = queryExpiredUser(); for (Long userId : userIdList) { User userUpdate = new User(); userUpdate.setId(userId); userUpdate.setEnable(Boolean.FALSE); userDAO.update(userUpdate); } }
建议方案
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void disableUser () { List<Long> userIdList = queryBlackUser(); for (Long userId : userIdList) { disableUser(userId); } userIdList = queryExpiredUser(); for (Long userId : userIdList) { disableUser(userId); } } private void disableUser (Long userId) { User userUpdate = new User(); userUpdate.setId(userId); userUpdate.setEnable(Boolean.FALSE); userDAO.update(userUpdate); }
5.2 案例二:封装相似代码为函数 封装相似代码为函数,差异性通过函数参数控制。
现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void adoptOrder (Long orderId) { Order orderUpdate = new Order(); orderUpdate.setId(orderId); orderUpdate.setStatus(OrderStatus.ADOPTED); orderUpdate.setAuditTime(new Date()); orderDAO.update(orderUpdate); } public void rejectOrder (Long orderId) { Order orderUpdate = new Order(); orderUpdate.setId(orderId); orderUpdate.setStatus(OrderStatus.REJECTED); orderUpdate.setAuditTime(new Date()); orderDAO.update(orderUpdate); }
建议方案
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void adoptOrder (Long orderId) { auditOrder(orderId, OrderStatus.ADOPTED); } public void rejectOrder (Long orderId) { auditOrder(orderId, OrderStatus.REJECTED); } private void auditOrder (Long orderId, OrderStatus orderStatus) { Order orderUpdate = new Order(); orderUpdate.setId(orderId); orderUpdate.setStatus(orderStatus); orderUpdate.setAuditTime(new Date()); orderDAO.update(orderUpdate); }
5.3 主要收益
6.封装获取参数值函数 6.1 案例一 现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean isPassed (Long userId) { double thisPassThreshold = PASS_THRESHOLD; if (Objects.nonNull(passThreshold)) { thisPassThreshold = passThreshold; } double passRate = getPassRate(userId); return passRate >= thisPassThreshold; }
建议方案
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public boolean isPassed (Long userId) { double thisPassThreshold = getPassThreshold(); double passRate = getPassRate(userId); return passRate >= thisPassThreshold; } private double getPassThreshold () { if (Objects.nonNull(passThreshold)) { return passThreshold; } return PASS_THRESHOLD; }
6.2 主要收益
7. 通过接口参数化封装相同逻辑 7.1 案例一 现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void sendAuditorSettleData () { List<WorkerSettleData> settleDataList = auditTaskDAO.statAuditorSettleData(); for (WorkerSettleData settleData : settleDataList) { WorkerPushData pushData = new WorkerPushData(); pushData.setId(settleData.getWorkerId()); pushData.setType(WorkerPushDataType.AUDITOR); pushData.setData(settleData); pushService.push(pushData); } } public void sendCheckerSettleData () { List<WorkerSettleData> settleDataList = auditTaskDAO.statCheckerSettleData(); for (WorkerSettleData settleData : settleDataList) { WorkerPushData pushData = new WorkerPushData(); pushData.setId(settleData.getWorkerId()); pushData.setType(WorkerPushDataType.CHECKER); pushData.setData(settleData); pushService.push(pushData); }
建议方案
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public void sendAuditorSettleData () { sendWorkerSettleData(WorkerPushDataType.AUDITOR, () -> auditTaskDAO.statAuditorSettleData()); } public void sendCheckerSettleData () { sendWorkerSettleData(WorkerPushDataType.CHECKER, () -> auditTaskDAO.statCheckerSettleData()); } public void sendWorkerSettleData (WorkerPushDataType dataType, WorkerSettleDataProvider dataProvider) { List<WorkerSettleData> settleDataList = dataProvider.statWorkerSettleData(); for (WorkerSettleData settleData : settleDataList) { WorkerPushData pushData = new WorkerPushData(); pushData.setId(settleData.getWorkerId()); pushData.setType(dataType); pushData.setData(settleData); pushService.push(pushData); } } private interface WorkerSettleDataProvider { public List<WorkerSettleData> statWorkerSettleData () ; }
7.2 主要收益
8.减少函数代码层级 如果要使函数优美,建议函数代码层级在1-4之间,过多的缩进会让函数难以阅读。
8.1 案例一:利用return提前返回函数 现象描述:
1 2 3 4 5 6 7 8 9 10 11 public Double getUserBalance (Long userId) { User user = getUser(userId); if (Objects.nonNull(user)) { UserAccount account = user.getAccount(); if (Objects.nonNull(account)) { return account.getBalance(); } } return null ; }
建议方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public Double getUserBalance (Long userId) { User user = getUser(userId); if (Objects.isNull(user)) { return null ; } UserAccount account = user.getAccount(); if (Objects.isNull(account)) { return null ; } return account.getBalance(); }
8.2 案例二:利用continue提前结束循环 现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public double getTotalBalance (List<User> userList) { double totalBalance = 0.0 D; for (User user : userList) { UserAccount account = user.getAccount(); if (Objects.nonNull(account)) { Double balance = account.getBalance(); if (Objects.nonNull(balance)) { totalBalance += balance; } } } return totalBalance; }
建议方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public double getTotalBalance (List<User> userList) { double totalBalance = 0.0 D; for (User user : userList) { UserAccount account = user.getAccount(); if (Objects.isNull(account)) { continue ; } Double balance = account.getBalance(); if (Objects.nonNull(balance)) { totalBalance += balance; } } return totalBalance; }
特殊说明
其它方式:在循环体中,先调用案例1的函数getUserBalance(获取用户余额),再进行对余额进行累加。
在循环体中,建议最多使用一次continue。如果需要有使用多次continue的需求,建议把循环体封装为一个函数。
8.3 案例三:利用条件表达式函数减少层级 请参考下一章的”案例2: 把复杂条件表达式封装为函数”
8.4 主要收益
代码层级减少,代码缩进减少;
模块划分清晰,方便阅读维护。
9.封装条件表达式函数 9.1 案例一:把简单条件表达式封装为函数 现象描述:
1 2 3 4 5 6 7 8 public double getTicketPrice (Date currDate) { if (Objects.nonNull(currDate) && currDate.after(DISCOUNT_BEGIN_DATE) && currDate.before(DISCOUNT_END_DATE)) { return TICKET_PRICE * DISCOUNT_RATE; } return TICKET_PRICE; }
建议方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public double getTicketPrice (Date currDate) { if (isDiscountDate(currDate)) { return TICKET_PRICE * DISCOUNT_RATE; } return TICKET_PRICE; } private static boolean isDiscountDate (Date currDate) { return Objects.nonNull(currDate) && currDate.after(DISCOUNT_BEGIN_DATE) && currDate.before(DISCOUNT_END_DATE); }
9.2 案例二:把复杂条件表达式封装为函数 现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public List<User> getRichUserList (List<User> userList) { List<User> richUserList = new ArrayList<>(); for (User user : userList) { UserAccount account = user.getAccount(); if (Objects.nonNull(account)) { Double balance = account.getBalance(); if (Objects.nonNull(balance) && balance.compareTo(RICH_THRESHOLD) >= 0 ) { richUserList.add(user); } } } return richUserList; }
建议方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public List<User> getRichUserList (List<User> userList) { List<User> richUserList = new ArrayList<>(); for (User user : userList) { if (isRichUser(user)) { richUserList.add(user); } } return richUserList; } private boolean isRichUser (User user) { UserAccount account = user.getAccount(); if (Objects.isNull(account)) { return false ; } Double balance = account.getBalance(); if (Objects.isNull(balance)) { return false ; } return balance.compareTo(RICH_THRESHOLD) >= 0 ; }
以上代码也可以用采用流式(Stream)编程的过滤来实现。
9.3 主要收益
10.尽量避免不必要的空指针判断 本章只适用于项目内部代码,并且是自己了解的代码,才能够尽量避免不必要的空指针判断。对于第三方中间件和系统接口,必须做好空指针判断,以保证代码的健壮性。
10.1 案例一:调用函数保证参数不为空,被调用函数尽量避免不必要的空指针判断 现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 User user = new User(); ... createUser(user); private void createUser (User user) { if (Objects.isNull(user)) { return ; } userDAO.insert(user); userRedis.save(user); }
建议方案:
1 2 3 4 5 6 7 8 9 10 11 User user = new User(); ... createUser(user); private void createUser (User user) { userDAO.insert(user); userRedis.save(user); }
10.2 案例二:被调用函数保证返回不为空,调用函数尽量避免不必要的空指针判断
现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void saveUser (Long id, String name) { User user = buildUser(id, name); if (Objects.isNull(user)) { throw new BizRuntimeException("构建用户信息为空" ); } userDAO.insert(user); userRedis.save(user); } private User buildUser (Long id, String name) { User user = new User(); user.setId(id); user.setName(name); return user; }
建议方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void saveUser (Long id, String name) { User user = buildUser(id, name); userDAO.insert(user); userRedis.save(user); } private User buildUser (Long id, String name) { User user = new User(); user.setId(id); user.setName(name); return user; }
10.3 案例三:赋值逻辑保证列表数据项不为空,处理逻辑尽量避免不必要的空指针判断 现象描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 List<UserDO> userList = userDAO.queryAll(); if (CollectionUtils.isEmpty(userList)) { return ; } List<UserVO> userVoList = new ArrayList<>(userList.size()); for (UserDO user : userList) { UserVO userVo = new UserVO(); userVo.setId(user.getId()); userVo.setName(user.getName()); userVoList.add(userVo); } for (UserVO userVo : userVoList) { if (Objects.isNull(userVo)) { continue ; } ... }
建议方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 List<UserDO> userList = userDAO.queryAll(); if (CollectionUtils.isEmpty(userList)) { return ; } List<UserVO> userVoList = new ArrayList<>(userList.size()); for (UserDO user : userList) { UserVO userVo = new UserVO(); userVo.setId(user.getId()); userVo.setName(user.getName()); userVoList.add(userVo); } for (UserVO userVo : userVoList) { ... }
10.4 案例四:MyBatis查询函数返回列表和数据项不为空,可以不用空指针判断 MyBatis是一款优秀的持久层框架,是在项目中使用的最广泛的数据库中间件之一。通过对MyBatis源码进行分析,查询函数返回的列表和数据项都不为空,在代码中可以不用进行空指针判断。
现象描述:
这种写法没有问题,只是过于保守了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public List<UserVO> queryUser (Long id, String name) { List<UserDO> userList = userDAO.query(id, name); if (Objects.isNull(userList)) { return Collections.emptyList(); } List<UserVO> voList = new ArrayList<>(userList.size()); for (UserDO user : userList) { if (Objects.isNull(user)) { continue ; } UserVO vo = new UserVO(); BeanUtils.copyProperties(user, vo); voList.add(vo); } return voList; }
建议方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public List<UserVO> queryUser (Long id, String name) { List<UserDO> userList = userDAO.query(id, name); List<UserVO> voList = new ArrayList<>(userList.size()); for (UserDO user : userList) { UserVO vo = new UserVO(); BeanUtils.copyProperties(user, vo); voList.add(vo); } return voList; }
10.5 主要收益
转自微信公众号:高德技术,作者:常意