實(shí)現(xiàn)代碼優(yōu)雅的十條心法
近期在項(xiàng)目中看了自己先前寫過的代碼,感觸最多的就是 Code Review 對(duì)開發(fā)效率、代碼整潔上的影響。這里總結(jié)下,內(nèi)容涵蓋了一些設(shè)計(jì)的思想,也提供了一些常用的代碼片段,非常實(shí)用。

開篇導(dǎo)讀

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















 
 
 











 
 
 
 