本文通过阅读《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 探索指南
微信扫一扫
关注该公众号