實現代碼優雅的十條心法
近期在項目中看了自己先前寫過的代碼,感觸最多的就是 Code Review 對開發效率、代碼整潔上的影響。這里總結下,內容涵蓋了一些設計的思想,也提供了一些常用的代碼片段,非常實用。
開篇導讀
基礎版
1.清晰的注釋
比如:在一個業務類中創建訂單接口,
執行流程:下訂單 -> 減庫存-> 支付-> 日志。通過合理的注釋可以獲得清晰的執行邏輯。當然,可在類、方法、屬性上使用注釋,但是注釋只是一個說明性的東西,能表達意思即可,不易過度使用。
/**
* 訂單業務實現類
*/
public class OrderService {
@Autowired
InventoryService inventoryService;
@Autowired
PaymentService paymentService;
@Autowired
LogService logService;
/**
* 創建訂單
* 執行下訂單、減庫存、支付處理和記錄日志的流程。
*/
public String createOrder(Order order) {
// 檢查訂單
if (!order.isValid()) {
return "訂單無效";
}
// 減庫存
if (!inventoryService.decreaseStock(order.getProduct(), order.getQuantity())) {
return "庫存不足";
}
// 支付
if (!paymentService.process(order.getPaymentInfo())) {
// 如果支付失敗,需要回滾庫存
inventoryService.increaseStock(order.getProduct(), order.getQuantity());
return "支付失敗";
}
// 訂單創建成功,記錄日志
logService.createOrderLog(order);
// 返回訂單創建成功的消息
return "訂單創建成功";
}
}
2.提高可讀性
可讀性較差的版本:可以看到業務邏輯混在一起,可讀性非常差。
public String process(int id, int num, double price) {
if (id > 0 && num > 0 && price > 0) {
Product p = productRepository.getProductById(id);
if (p != null && p.getStock() >= num) {
boolean isPaymentSuccess = paymentService.pay(price);
if (isPaymentSuccess) {
productRepository.updateStock(id, p.getStock() - num);
return "Order processed";
}
}
return "Error";
}
return "Invalid param";
}
可讀性較好的版本:在于方法的精細化抽象。清晰的命名和表達。如:(1)小節的實現。
3.命名規范
(1) 見名知意
比如,有個方法是校驗邏輯,
正例:
//清晰的命名,見名知義
void validOrder(Order order);
反例:
//命名隨意,寫時一時爽,日后莫相忘
void methodA();
(2) 常量命名
常量命名全部大寫,單詞間用下劃線隔開,力求語義表達完整清楚。
public class BaseInfo {
/**
* 業務標識
*/
public final static String BIZ_CODE = "XXXXXX";
}
(3) 峰命名
方法名、參數名、成員變量、局部變量都統一使用lowerCamelCase風格。
正例:
getHttpMessage()
(4) 實體類命名 :VO DO DTO POJO
使用場景:
- POJO:作為簡單的數據結構,不包含業務邏輯。
- DO:在業務邏輯層使用,代表業務實體。
- DTO:在需要將數據從一個層傳輸到另一個層,或者在遠程調用中使用。
- VO:在展示層使用,用于將數據展示給用戶。
4.使用輪子
Java類庫中有很多常用的方法,也有一些開源的工具類等供使用。下面句幾個例子:
(1) Lambda表達式
主要針對函數式接口,簡化代碼使用。不過使用過程中注意空指針的一些問題
案例:統計北京地區(按區劃分) ,年齡在25~30 歲的,月薪超過20k 的人群 中薪資最高的程序員 組合成一個Map, 地區為key , 每個value 是一個List. 用Lambda 實現
public static void main(String[] args) {
// 模擬的程序員列表
List<Developer> developers = new ArrayList<>();
developers.add(new Developer("Alice", "北京朝陽區", 28, 22000, "男"));
developers.add(new Developer("Alice", "北京朝陽區", 25, 28000, "男"));
developers.add(new Developer("Bob", "北京海淀區", 24, 23000, "男"));
developers.add(new Developer("Bob", "北京海淀區", 29, 30000, "男"));
developers.add(new Developer("Charlie", "北京朝陽區", 31, 25000, "男"));
// 根據條件過濾程序員,并按地區分組,每個地區選擇薪資最高的程序員
Map<String, Optional<Developer>> map = developers.stream()
.filter(d -> d.getLocation().startsWith("北京")) // 北京地區
.filter(d -> d.getAge() >= 25 && d.getAge() <= 30) // 年齡在25~30歲
.filter(d -> d.getSalary() > 20000) // 月薪超過20k
.collect(Collectors.groupingBy(Developer::getLocation,
// 對每個地區的程序員按薪資進行排序,選擇薪資最高的程序員
LinkedHashMap::new, // 保持插入順序,有助于選擇薪資最高的
Collectors.maxBy(Comparator.comparing(Developer::getSalary))));
// 運行
map.forEach((district, highestPaidDev) -> {
System.out.println("地區: " + district);
highestPaidDev.ifPresent(
dev -> System.out.println("薪資最高的程序員: " + dev)
);
});
}
// 運行結果:
// 地區: 北京朝陽區
// 薪資最高的程序員: Developer{name='Alice', location='北京朝陽區', age=25, salary=28000.0, gender='男'}
//地區: 北京海淀區
//薪資最高的程序員: Developer{name='Bob', location='北京海淀區', age=29, salary=30000.0, gender='男'}
(2) 使用Lombook
主要用于一些實體類,簡化代碼:
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Orders {
private Integer id;
private Integer orderType;
private Integer customerId;
private Double amount;
private Integer status;
private Date createDate;
private Date updateDate;
//省略了 構造函數、getter,setter方法等
}
當然使用過程可能存在一些問題,讀者自行摸索,但是從簡化的角度講,確實顯得干凈整潔。
(3) 使用工具類Hutool
Hutool提供了很多好用的工具類,也針對其做了大量封裝。用于日常業務開發。
官方文檔:Hutool參考文檔
例如: 日期時間偏移 日期或時間的偏移指針對某個日期增加或減少分、小時、天等等,達到日期變更的目的。
String dateStr = "2017-03-01 22:33:23";
//結果:2017-03-03 22:33:23
Date newDate = DateUtil.offset(date, DateField.DAY_OF_MONTH, 2);
5.優雅的異常處理
異常用來定位問題使用,常見的異常處理 是在業務方法里捕獲。
try {
// 業務邏輯
} catch (Exception e) {
log.error("業務邏輯異常:{}", e.fillInStackTrace());
} finally {
// 不管是否發生異常,finally塊中的代碼都會執行
}
實際上,可以通過全局異常處理方式實現。大致思路如, 使用細節請自行搜索。
/**
* @ClassName GlobalExceptionHandler
* @Author weiweiyixiao
* @Description @ControllerAdvice + @ExceptionHandler 實現全局的 Controller 層的異常處理
* @Date 2024/7/4 22:53
* @Version 1.0
*/
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 處理自定義異常
*/
@ExceptionHandler(value = DefinitionException.class)
@ResponseBody
public Result bizExceptionHandler(DefinitionException e) {
return Result.defineError(e);
}
/**
* 處理其他異常
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result exceptionHandler(Exception e) {
return Result.otherError(ErrorEnum.INTERNAL_SERVER_ERROR);
}
}
進階版
到了這一章,我們從整體上考量程序的設計的健壯性、拓展性。常見的有方案:AOP思想、代碼分層、公共抽離、設計模式等。
6.AOP思想
AOP思想 是程序解耦的一種重要手段,常用來記錄日志,
// 定義一個切面類
@Aspect
@Component
public class LoggingAspect {
// 定義一個切入點,這里以所有以"handle"開頭的方法為例
@Pointcut("execution(* com.yourpackage.service.*.handle*(..))")
private void handleMethod() {}
// 在方法執行之前記錄日志
@Before("handleMethod()")
public void logBefore(JoinPoint joinPoint) {
// 獲取被攔截的方法名
String methodName = joinPoint.getSignature().getName();
// 可以獲取方法的參數,這里以第一個參數為例
Object[] args = joinPoint.getArgs();
// 打印日志
System.out.println("Before method: " + methodName + ", with arguments: " + Arrays.toString(args));
}
// 在方法執行之后記錄日志
@After("handleMethod()")
public void logAfter(JoinPoint joinPoint) {
// 獲取被攔截的方法名
String methodName = joinPoint.getSignature().getName();
// 打印日志
System.out.println("After method: " + methodName);
}
}
7.代碼解耦
可以使用注解+AOP 輔助實現
比如:實現一個接口調用時間的統計。
低配版:
public BaseRspsMsg getBusiInfo() {
BaseRspsMsg baseRspsMsg = new BaseRspsMsg();
//開始時間
long startTime = System.currentTimeMillis();
try {
//從數據庫查
} catch (Exception e) {
//異常處理
}
//結束時間
long endSendTime = System.currentTimeMillis();
log.info(" 執行結束耗時:{}S", (endSendTime - startTime) / 1000);
return baseRspsMsg;
}
高配版:使用注解+AOP
//定義注解實現時間統計
@Aspect
@Slf4j
@Component
public class TimeAspect {
//掃描對應標有注解的方法
@Around(value = "@annotation(com.business.annot.TimeStatistics)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
String methodName = joinPoint.getSignature().getName();
stopWatch.start(methodName);
try {
return joinPoint.proceed();
} finally {
stopWatch.stop();
log.info("======= 調用【" + methodName + "】方法執行耗時 ======= {} s", stopWatch.getTotalTimeSeconds());
}
}
}
在需要統計方法上加一個注解即可輕松實現。
@TimeStatistics
public BaseRspsMsg getBusiInfo() {
BaseRspsMsg baseRspsMsg = new BaseRspsMsg();
//開始時間
long startTime = System.currentTimeMillis();
try {
//從數據庫查
} catch (Exception e) {
//異常處理
}
return baseRspsMsg;
}
看,一處定義,隨處使用。是不是很清爽?
8.模型抽離
這個主要針對工程設計,從項目全局的角度考量。一般微服務項目設計也是這樣的。主要提及:
工程結構:
project
|___ pro-base //版本定義
|___ pro-core //業務核心數據流
|___ pro-common //公共實體,工具類、Feign定義、通用方法等
|___ pro-dao //數據層邏輯
|___ ..... //其它業務服務
9.設計模式
設計模式 的使用也是為了解耦,符合代碼設計的 單一職責、開閉原則 。
關于常用的幾種設計模式,讀者可參閱原來的一篇文章。
10.接口隔離
接口隔離原則強調的是接口設計應該滿足特定的客戶端需求,將大型接口拆分成更小的、特定的接口. 和高內聚的思想相輔相成。
比如:一個電子商務平臺,該平臺需要處理不同類型的支付方式。
假設我們有一個Payment接口,它包含了所有支付方式的通用方法:
public interface Payment {
void pay(double amount);
void refund(double amount);
}
這個接口被兩個類實現:CreditCardPayment和PayPalPayment。它們都實現了Payment接口,即使PayPalPayment可能不需要refund方法。
public class CreditCardPayment implements Payment {
public void pay(double amount) {
// 實現信用卡支付邏輯
}
public void refund(double amount) {
// 實現信用卡退款邏輯
}
}
public class PayPalPayment implements Payment {
public void pay(double amount) {
// 實現PayPal支付邏輯
}
// PayPalPayment 實際上不需要實現 refund 方法
public void refund(double amount) {
throw new UnsupportedOperationException("Refunds not supported for PayPal");
}
}
應用接口隔離原則
我們可以將Payment接口拆分成兩個更具體的接口:
public interface ICreditCardPayment {
void pay(double amount);
void refund(double amount);
}
public interface IPayPalPayment {
void pay(double amount);
}
然后,我們修改CreditCardPayment和PayPalPayment類,讓它們實現相應的接口:
public class CreditCardPayment implements ICreditCardPayment {
public void pay(double amount) {
// 實現信用卡支付邏輯
}
public void refund(double amount) {
// 實現信用卡退款邏輯
}
}
public class PayPalPayment implements IPayPalPayment {
public void pay(double amount) {
// 實現PayPal支付邏輯
}
}
由此可見, 通過應用接口隔離原則,我們減少了類之間的不必要依賴,提高了系統的靈活性和可維護性。每個類只實現了它需要的接口,而不是一個大而全的接口,這使得代碼更加清晰和易于理解。