Java 程序員如何優雅編程

導讀

本文結合作者經驗提出了一些編程的建議,這些建議旨在告訴讀者如何更好的構造代碼以便於它們能更好的工作,也便於將來對代碼進行修改和改善的時候有一個參考。甚至,程序也會因此變得更加令人愉悅,更加優雅。

01 前言

在今年的敏捷團隊建設中,我通過 Suite 執行器實現了一鍵自動化單元測試。Juint 除了 Suite 執行器還有哪些執行器呢?由此我的 Runner 探索之旅開始了!

編程不僅是一項單純的技能,更是一個充滿創造力的活動,能夠使用代碼與人進行雙向互動,是一門真正的藝術。編碼風格是編寫優雅代碼不可或缺的一環,好的編碼風格有助於降低團隊溝通成本。接下來讓我們進步一探討如何優雅編程。

02 優雅編程方式

理解,首先 MCube 會依據模板緩存狀態判斷是否需要網絡獲取最新模板,當獲取到模板後進行模板加載,加載階段會將產物轉換爲視圖樹的結構,轉換完成後將通過表達式引擎解析表達式並取得正確的值,通過事件解析引擎解析用戶自定義事件並完成事件的綁定,完成解析賦值以及事件綁定後進行視圖的渲染,最終將目標頁面展示到屏幕

2.1 優雅防重

如果業務體系滿足以下兩個條件:

  1. 業務接口重複調用的概率不是很高。

  2. 入參有明確業務主鍵如:訂單 ID,商品 ID,文章 ID,運單 ID 等。

在這種場景下,非常適合樂觀防重,思路就是代碼處理不主動做防重,只在監測到重複提交後做相應處理。如何監測到重複提交呢?

MySQL 唯一索引 + org.spring framework. dao. Duplicate Key Exception。

代碼如下:

public int createContent(ContentOverviewEntity contentEntity) {
  try{
    return contentOverviewRepository.createContent(contentEntity);
  }catch (DuplicateKeyException dke){
    log.warn("repeat content:{}",contentEntity.toString());
  }
  return 0;
}

2.2 用好 Stream

初級程序員向中級進階的必經之路就是攻克 Stream。Stream 和麪向對象編程是兩個編程理念,《架構整潔之道》裏曾提到有三種編程範式,結構化編程(面向過程編程)、面向對象編程、函數式編程。初次接觸 Stream 肯定特別不適應,但如果熟悉以後將打開一個編程方式的新思路。本文 Stream,只講如下例子:

比如,如果想把一個二維表數據進行分組,可採用以下一行代碼實現:

List<ActionAggregation> actAggs = ....
Map<String, List<ActionAggregation>> collect = 
    actAggs.stream()
    .collect(Collectors.groupingBy(ActionAggregation :: containWoNosStr,LinkedHashMap::new,Collectors.toList()));

2.3 用好衛語

各個大場的 JAVA 編程規範裏基本都有這條建議,但真正用好它的不多,衛語句對提升代碼的可維護性有着很大的作用,想像一下,在一個 10 層 if 縮進的接口裏找代碼邏輯是一件多麼痛苦的事情。筆者曾在一個微服務裏的一個核心接口看到了這種代碼,該接口被過多的人接手導致了這樣的局面。系統接手人過多以後,代碼腐化的速度超出想像。

下面舉例說明:

沒有用衛語句的代碼,很多層縮進:

if (title.equals(newTitle)){
  if (...) {
    if (...) {
      if (...) {
      }
    }else{
    }
  }else{
  }
}

使用了衛語句的代碼,縮進很少:

if (!title.equals(newTitle)) {
  return xxx;
}
if (...) {
  return xxx;
}else{
  return yyy;
}
if (...) {
  return zzz;
}

2.4 避免雙重循環

簡單說雙重循環會將代碼邏輯的時間複雜度擴大至 O(n^2)。

如果有按 key 匹配兩個列表的場景建議使用以下方式:

  1. 將列表 1 進行 map 化。

  2. 循環列表 2,從 map 中獲取值。

代碼示例如下:

List<WorkOrderChain> allPre = ...
List<WorkOrderChain> chains = ...
Map<String, WorkOrderChain> preMap = allPre.stream().collect(Collectors.toMap(WorkOrderChain::getWoNext, item -> item,(v1, v2)->v1));
chains.forEach(item->{
  WorkOrderChain preWo = preMap.get(item.getWoNo());
  if (preWo!=null){
    item.setIsHead(1);
  }else{
    item.setIsHead(0);
  }
});

2.5 用 @see @link 來設計 RPC 的 API

程序員們還經常自嘲的幾個詞有:API 工程師,中間件裝配工等,既然平時寫 API 寫的比較多,那種就把它寫到極致 @see @link 的作用是讓使用方可以方便的鏈接到枚舉類型的對象上,方便閱讀。

示例如下:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ContentProcessDto implements Serializable {
    /**
     * 內容ID
     */
    private String contentId;
    /**
     * @see com.jd.jr.community.common.enums.ContentTypeEnum
     */
    private Integer contentType;
    /**
     * @see com.jd.jr.community.common.enums.ContentQualityGradeEnum
     */
    private Integer qualityGrade;
}

2.6 日誌打印避免只打整個參數

研發經常爲了省事,直接將入參這樣打印:

log.info("operateRelationParam:{}", JSONObject.toJSONString(request));

該日誌進了日誌系統後,研發在搜索日誌的時候,很難根據業務主鍵排查問題

如果改進成以下方式,便可方便的進行日誌搜索。

log.info("operateRelationParam,id:{},req:{}", request.getId(),JSONObject.toJSONString(request));

如上:只需要全詞匹配 “operateRelationParam,id:111”,即可找到業務主鍵 111 的業務日誌。

2.7 用異常捕捉替代方法參數傳遞

程序員經常面對的一種情況是:從子方法中獲取返回的值來標識程序接下來的走向,這種方式筆者認爲不夠優雅。

舉例:以下代碼 paramCheck 和 deleteContent 方法,返回了這兩個方法的執行結果,調用方通過返回結果判斷程序走向。

public RpcResult<String> deleteContent(ContentOptDto contentOptDto) {
    log.info("deleteContentParam:{}", contentOptDto.toString());
    try{
        RpcResult<?> paramCheckRet = this.paramCheck(contentOptDto);
        if (paramCheckRet.isSgmFail()){
            return RpcResult.getSgmFail("非法參數:"+paramCheckRet.getMsg());
        }
        ContentOverviewEntity contentEntity = DozerMapperUtil.map(contentOptDto,ContentOverviewEntity.class);
        RpcResult<?> delRet = contentEventHandleAbility.deleteContent(contentEntity);
        if (delRet.isSgmFail()){
            return RpcResult.getSgmFail("業務處理異常:"+delRet.getMsg());
        }
    }catch (Exception e){
        log.error("deleteContent exception:",e);
        return RpcResult.getSgmFail("內部處理錯誤");
    }
    return RpcResult.getSgmSuccess();
    }

可以通過自定義異常的方式解決:子方法拋出不同的異常,調用方 catch 不同異常以便進行不同邏輯的處理,這樣調用方特別清爽,不必做返回結果判斷。

代碼示例如下:

public RpcResult<String> deleteContent(ContentOptDto contentOptDto) {
  log.info("deleteContentParam:{}", contentOptDto.toString());
  try{
      this.paramCheck(contentOptDto);
    ContentOverviewEntity contentEntity = DozerMapperUtil.map(contentOptDto,ContentOverviewEntity.class);
    contentEventHandleAbility.deleteContent(contentEntity);    
  }catch(IllegalStateException pe){
    log.error("deleteContentParam error:"+pe.getMessage(),pe);
    return RpcResult.getSgmFail("非法參數:"+pe.getMessage());
  }catch(BusinessException be){
    log.error("deleteContentBusiness error:"+be.getMessage(),be);
    return RpcResult.getSgmFail("業務處理異常:"+be.getMessage());
  }catch (Exception e){
    log.error("deleteContent exception:",e);
    return RpcResult.getSgmFail("內部處理錯誤");
  }
  return RpcResult.getSgmSuccess();
}

2.8 自定義 Spring Boot 的 Banner

別再讓 Spring Boot 啓動 banner 千篇一律,spring 支持自定義 banner,該技能對業務功能實現沒任何卵用,但會給枯燥的編程生活添加一點樂趣。

以下是官方文檔的說明:

https: // docs .spring. io/ spring -boot /docs /1.3.8. RELEASE /reference /htmlsingle /# boot -features - banner

另外還需要 ASCII 藝術字生成工具:https://tools.kalvinbg.cn/txt/ascii

效果如下:

圖 1 ASCII 藝術字效果圖

2.9 多用 JAVA 語法糖

編程語言中 java 的語法是相對繁瑣的,用過 golang 的或 scala 的人感覺特別明顯。java 提供了 10 多種語法糖,寫代碼常使用語法糖,給人一種 “這哥們 java 用得通透” 的感覺。

舉例:try-with-resource 語法,當一個外部資源的句柄對象實現了 AutoCloseable 接口,JDK7 中便可以利用 try-with-resource 語法更優雅的關閉資源,消除板式代碼。

try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
    System.out.println(inputStream.read());
} catch (IOException e) {
    throw new RuntimeException(e.getMessage(), e);
}

2.10 利用鏈式編程

鏈式編程,也叫級聯式編程,調用對象的函數時返回一個 this 對象指向對象本身,達到鏈式效果,可以級聯調用。鏈式編程的優點是:編程性強、可讀性強、代碼簡潔。

舉例:假如覺得官方提供的容器不夠方便,可以自定義,代碼如下,但更建議使用開源的經過驗證的類庫如 guava 包中的工具類:

/**
    鏈式map
 */
public class ChainMap<K,V> {
    private Map<K,V> innerMap = new HashMap<>();
    public V get(K key) {
        return innerMap.get(key);
    }
    public ChainMap<K,V> chainPut(K key, V value) {
        innerMap.put(key, value);
        return this;
    }
    public static void main(String[] args) {
        ChainMap<String,Object> chainMap = new ChainMap<>();
        chainMap.chainPut("a","1")
                .chainPut("b","2")
                .chainPut("c","3");
    }
}

2.11 優雅暫停線程

Thread.sleep(long timeout) 暫停線程時必須捕獲或向上層拋出 InterruptedException,同時如果被中斷,則滿足不了預期的 timeout 時間,Guava 包中有個方法:com. google .common. util. concurrent. Uninterruptibles #sleep Uninterruptibly,可以優雅的讓線程暫停,不僅不用關心異常,同時如果被中斷,還會補償,直到 time out 時間耗盡。

2.12 適時使用元組(tuple)對象

接口返回值往往是成對的,或者是成三的,此時有幾種做法:

1、通過 map 格式返回,

2、自定義新對象將返回的元素包裝起來,此兩種做法都能解決問題,

但筆者推薦另一種方法:使用元組對象 apache common 包裏內置了二元組和三元組可供使用。

org.apache.commons.lang3.tuple.Pair
org.apache.commons.lang3.tuple.Triple

當需要 4 元組、5 元組的時候,可以去自定義新對象。元組維度太多也不利於代碼可維護性。(scala 語言內置了 1-20 個維度的元組供研發使用)。

03  總結

本文立足於編碼規範之上,從研發角度探討如何優雅編程,羅列一些策略,如衛語句使用、註解設計 API、異常捕獲特殊用法、鏈式編程等,基於這些策略可以使代碼更加優雅易維護。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8bYUGcojuy7eMGZxvbxl-A