本文通过阅读《Effective Java》、《Clean Code》、《京东JAVA代码规范》等代码质量书籍,结合团队日常代码实践案例进行整理,抛砖引玉、分享一些在编写高质量代码方面的见解和经验。这些书籍提供了丰富的理论知识,而团队的实际案例则展示了这些原则在实际开发中的应用。希望通过这篇文章,能够帮助大家更好地理解和运用这些编程最佳实践,提高代码质量和开发效率。
在 Java 中,方法是类的一部分,定义了类的行为。方法通常包含方法头和方法体。方法头包括访问修饰符、返回类型、方法名和参数列表,而方法体包含实现方法功能的代码。
方法的基本结构
[访问修饰符] [返回类型] [方法名]([参数列表]) {
// 方法体
// 实现方法功能的代码
❌错误案例:重量/体积 同类型参数顺序错误导致问题// 错误的方法定义,参数过多且容易混淆public void calculateShippingCost(double weight, double volume, double length, double width, double height, String destination) { // 假设这里有计算运费的逻辑}// 这里将重量和体积的顺序弄反了service.calculateShippingCost(30.0, 50.0, 10.0, 5.0, 3.0, "New York");// 实际上应该是:service.calculateShippingCost(50.0, 30.0, 10.0, 5.0, 3.0, "New York");public class ShippingDetails { private double weight; private double volume; private double length; private double width; private double height; private String destination; // 构造方法、getter和setter省略}// 使用参数对象来简化方法签名public void calculateShippingCost(ShippingDetails details) { // 假设这里有计算运费的逻辑}
通过将参数封装成一个类,可以有效减少方法的参数数量,避免参数顺序错误的问题,提高代码的可读性和可维护性。
❌错误案例:循环中调用可变参数方法public class Logger { // 可变参数方法 public void log(String level, String... messages) { StringBuilder sb = new StringBuilder(); sb.append(level).append(": "); for (String message : messages) { sb.append(message).append(" "); } System.out.println(sb.toString()); }}// 模拟高频调用for (int i = 0; i < 1000000; i++) { logger.log("INFO", "Message", "number", String.valueOf(i));}
在这个案例中,log方法每次调用都会创建一个新的数组来保存可变参数messages。在高频调用的场景下,这种数组分配和初始化的开销会显著影响性能。public class Logger { // 使用List代替可变参数 public void log(String level, List<String> messages) { StringBuilder sb = new StringBuilder(); sb.append(level).append(": "); for (String message : messages) { sb.append(message).append(" "); } System.out.println(sb.toString()); }}// 模拟高频调用for (int i = 0; i < 1000000; i++) { logger.log("INFO", List.of("Message", "number", String.valueOf(i)));}public class Logger { // 使用StringBuilder直接拼接 public void log(String level, String message1, String message2, String message3) { StringBuilder sb = new StringBuilder(); sb.append(level).append(": ") .append(message1).append(" ") .append(message2).append(" ") .append(message3).append(" "); System.out.println(sb.toString()); }}// 模拟高频调用for (int i = 0; i < 1000000; i++) { logger.log("INFO", "Message", "number", String.valueOf(i));}
如果无法承受上面的性能开销,但又需要可变参数的便利性,可以有一种兼容的做法,假设方法95%的调用参数不超过3个,那么我们可以声明该方法的5个重载版本,分别包含(0,1,2,3)个参数和一个(3,可变参数),这样只有最后一个方法才需要付出创建数组的开销,而这只占用5%的调用。package org.slf4j;public interface Logger { public boolean isInfoEnabled(); public void info(String msg); public void info(String format, Object arg); public void info(String format, Object arg1, Object arg2); public void info(String format, Object... arguments); public void info(String msg, Throwable t);}
✅案例:链路校验一致
比如某个入参,从上游到整个链路下游,包括方法内部链路,最终到数据库存储,校验规则是一致的。在下面这个例子中,userName的长度限制在方法入口和数据库存储过程中保持一致,确保链路校验一致。public class UserService { // 用户信息保存方法 public void saveUser(String userName) { // 参数校验 if (userName == null || userName.length() > 20) { throw new IllegalArgumentException("User name cannot be null and must be less than 20 characters"); } // 假设数据库字段长度限制为 20 saveToDatabase(userName); } private void saveToDatabase(String userName) { // 数据库保存逻辑 // ... }} // 假设数据库字段长度限制为 10 private void saveToDatabase(String userName) { // 数据库保存逻辑 // ... }
public int filterBusinessType( Request request,Response response) {if(...){return ...}boolean flag = isXXX(request, response);}
正如上面说的方法职责单一,只做一件事,但副作用就是一个谎言,方法还会做其他隐藏起来的事情,我们需要理解副作用的存在,并采取合适的策略来管理和控制它们。
1.分离关注点: 可以将获取业务类型和响应设置分离成两个不同的方法。这样,调用者就可以清晰地看到每个方法的职责。public int filterBusinessType(String logPrefix,Request request){ // 过滤逻辑... int businessType=...; return businessType;}public void setResponseData(int filterResult,Response response){ // 根据过滤结果设置响应数据... response.setFilteredData(...);}public FilterResultAndResponse filterBusinessType(String logPrefix,Request request){ // 过滤逻辑... int result=...; Response response=new Response(); response.setFilteredData(...); return new FilterResultAndResponse(result, response);}class FilterResultAndResponse{ private int filterResult; private Response response; public FilterResultAndResponse(int filterResult,Response response){ this.filterResult = filterResult; this.response = response; } // Getters and setters for filterResult and response}
不要在条件判断中执行复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量,以提高可读性。团队中也存在很多if语句内的逻辑相当复杂,阅读者需要分析条件表达式的最终结果,才能明确什么样的条件执行什么样的语句。复杂逻辑表达式,与、或、取反混合运算,甚至各种方法纵深调用,理解成本非常高。如果赋值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情boolean flagA = isKaWhiteFlag(logPrefix, request);boolean flagB = PlatformTypeEnum.JD_STATION.getValue() == request.getPlatformType();boolean flagC = KaPromiseUccSwitch.isPopJDDeliverySwitch(request.getDict(),request.getStoreId()) && (PlatformTypeEnum.JD_STATION.getValue() == request.getPlatformType()) && (DeliveryTypeEnum.JD_DELIVERY.getType() == request.getDeliveryType());if (!flagC && flagA) {......}else if (!flagB && !flagC && StringUtils.isNotBlank(request.getProductCode()) && kaPromiseSwitch.isKaStoreRouterDs(logPrefix.getLogPrefix(), request.getDict(), request.getStoreId(), request.getCalculateTime(),request.getDeptNo())){......}else{......}
// 使用异常处理来控制流程public static int parseNumber(String number) {try {return Integer.parseInt(number);} catch (NumberFormatException e) {throw e;}}
// 使用常规控制结构来处理正常流程public static boolean isNumeric(String str) {if (str == null) {return false;}try {Integer.parseInt(str);return true;} catch (NumberFormatException e) {return false;}}
❌错误案例 try { // 可能抛出IOException throw new IOException("File not found"); } catch (IOException e) { // 空的catch块,忽略异常 }
对于业务层面的异常,应当进行适当的封装,定义统一的异常模型。避免直接将底层异常暴露给上层模块,以保持业务逻辑的清晰性。比如DependencyFailureException:表示服务端依赖的其他服务出现错误,服务端是不可用的,可以尝试重试,类比HTTP的5XX响应状态码。InternalFailureException:表示服务端自身出现错误,服务端是不可用的,可以尝试重试,类比HTTP的5XX响应状态码。
1.Web 层绝不应该继续往上抛异常,因为已经处于顶层,无继续处理异常的方式,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面,加上友好的错误提示信息。
5XX(服务端错误):表示服务器在处理请求的过程中发生了错误。例如,500表示服务器内部错误,无法完成请求。
少:少即是多,日志太多第一影响性能,第二存储成本,第三影响排查
7.在 Service 层出现异常时,必须记录出错日志到磁盘,其中日志记录应该遵循一定的规范,包括错误码、异常信息和必要的上下文信息。日志内容应该清晰明了,相当于保护案发现场。
❌案例:团队日志我一直想治理,其中2个痛点:第一个是打印的太多,第二个是很多日志只有当事人能看懂,其他成员看不懂
3.人员变更:团队成员的变动使得代码的可读性和可维护性变得更加重要。
4.描述副作用:如果方法有任何副作用,如启动后台线程或修改入参对象的某个值,这些都应该在注释中详细说明。这可以帮助调用者预见和处理可能的影响。public int filterBusinessType( Request request,Response response) { /** * 切记:return必须在下面这行代码(isXXX方法)后面,因为外面会使用response.A()来判断逻辑 * 你可以理解本filterBusinessType方法会返回业务类型,同时如果isXXX方法会修改response.setA()属性 */ boolean flag = isXXX(request, response); if(...){ return ... } }
对外API文档
✅案例:针对时效内核,代码比较抽象,添加的详细注释详细,加一下case案例,方便新人可读性
❎注意点:
1、注释会撒谎,代码注释的时间越久,就离其代码的本意越远,越来越变得错误,原因很简单:程序员不能坚持维护注释。
Taro 鸿蒙技术内幕系列(一):如何将 React 代码跑在 ArkUI 上
AI对话魔法|Prompt Engineering 探索指南
微信扫一扫
关注该公众号