Java方法设计原则与实践:从Effective Java到团队案例
01 背景 理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将 本文通过阅读《Effective Java》、《Clean Code》、《京东JAVA代码规范》等代码质量书籍,结合团队日常代码实践案例进行整理,抛砖引玉、分享一些在编写高质量代码方面的见解和经验。这些书籍提供了丰富的理论知识,而团队的实际案例则展示了这些原则在实际开发中的应用。希望通过这篇文章,能够帮助大家更好地理解和运用这些编程最佳实践,提高代码质量和开发效率。
本文通过阅读《Effective Java》、《Clean Code》、《京东JAVA代码规范》等代码质量书籍,结合团队日常代码实践案例进行整理,抛砖引玉、分享一些在编写高质量代码方面的见解和经验。这些书籍提供了丰富的理论知识,而团队的实际案例则展示了这些原则在实际开发中的应用。希望通过这篇文章,能够帮助大家更好地理解和运用这些编程最佳实践,提高代码质量和开发效率。
02 什么是一个好的方法
在 Java 中,方法是类的一部分,定义了类的行为。方法通常包含方法头和方法体。方法头包括访问修饰符、返回类型、方法名和参数列表,而方法体包含实现方法功能的代码。
方法的基本结构
[访问修饰符] [返回类型] [方法名]([参数列表]) {
// 方法体
// 实现方法功能的代码
入参
1)入参不要太多
❌错误案例:重量/体积 同类型参数顺序错误导致问题
// 错误的方法定义,参数过多且容易混淆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) {// 假设这里有计算运费的逻辑}
通过将参数封装成一个类,可以有效减少方法的参数数量,避免参数顺序错误的问题,提高代码的可读性和可维护性。
2)谨慎使用可变参数
❌错误案例:循环中调用可变参数方法
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);}
3)校验参数的有效性
技术指标:个人理解入参非法不应该体现到UMP技术可用率指标,因为这是API正常的一种体现,如果入参非法不合理,返回上游对应的错误码CODE,本身的技术可用率正常。 业务指标:但方法对应的业务指标可以反映入参非法的情况。例如,可以记录非法入参的次数,以便分析和改进整个链路的业务逻辑。
✅案例:链路校验一致 比如某个入参,从上游到整个链路下游,包括方法内部链路,最终到数据库存储,校验规则是一致的。在下面这个例子中,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");}// 假设数据库字段长度限制为 20saveToDatabase(userName);}private void saveToDatabase(String userName) {// 数据库保存逻辑// ...}}
// 假设数据库字段长度限制为 10private void saveToDatabase(String userName) {// 数据库保存逻辑// ...}
比如物流链路运单合法性校验,N个系统都进行校验是否有必要?是否应该只在入口处校验,其他链路保持信任机制?
1)方法要短小
2)无副作用
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}
3)控制语句(if/else/while/for等)
不要在条件判断中执行复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量,以提高可读性。团队中也存在很多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{......}
4)异常
4.1)异常应该仅用于异常的情况,不应该用于普通的控制流程
// 使用异常处理来控制流程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;}}
4.2)不要忽略异常
❌错误案例
try {// 可能抛出IOExceptionthrow new IOException("File not found");} catch (IOException e) {// 空的catch块,忽略异常}
4.3)异常封装
对于业务层面的异常,应当进行适当的封装,定义统一的异常模型。避免直接将底层异常暴露给上层模块,以保持业务逻辑的清晰性。比如DependencyFailureException:表示服务端依赖的其他服务出现错误,服务端是不可用的,可以尝试重试,类比HTTP的5XX响应状态码。InternalFailureException:表示服务端自身出现错误,服务端是不可用的,可以尝试重试,类比HTTP的5XX响应状态码。
4.4)异常转换
1.Web 层绝不应该继续往上抛异常,因为已经处于顶层,无继续处理异常的方式,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面,加上友好的错误提示信息。
2XX(成功响应):表示操作被成功接收并处理。例如,200表示请求成功。 4XX(客户端错误):表示请求包含语法错误或无法完成请求。例如,404表示请求的资源(网页等)不存在。 5XX(服务端错误):表示服务器在处理请求的过程中发生了错误。例如,500表示服务器内部错误,无法完成请求。
5)日志
5.1)日志三字经:准、懂、少
少:少即是多,日志太多第一影响性能,第二存储成本,第三影响排查
5.2)日志注意事项
7.在 Service 层出现异常时,必须记录出错日志到磁盘,其中日志记录应该遵循一定的规范,包括错误码、异常信息和必要的上下文信息。日志内容应该清晰明了,相当于保护案发现场。
❌案例:团队日志我一直想治理,其中2个痛点:第一个是打印的太多,第二个是很多日志只有当事人能看懂,其他成员看不懂
6)详细的注释
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、注释会撒谎,代码注释的时间越久,就离其代码的本意越远,越来越变得错误,原因很简单:程序员不能坚持维护注释。
返回空的集合或者数组,而不是null
本文由高可用架构转载。技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿