碼農如何提高自己的品味

語言: CN / TW / HK

作者:京東科技 文濤

前言

軟體研發工程師俗稱程式設計師經常對業界外的人自謙作碼農,一來給自己不菲的收入找個不錯的說辭(像農民伯伯那樣辛勤耕耘掙來的血汗錢),二來也是自嘲這個行業確實辛苦,辛苦得沒時間捯飭,甚至沒有駝背、脫髮加持都說不過去。不過時間久了,行外人還真就相信了程式設計師就是一幫沒品味,木訥的low貨,大部分的文藝作品中也都是這麼表現程式設計師的。可是我今天要說一下我的感受,程式設計是個藝術活,程式設計師是最聰明的一群人,我們的品味也可以像藝術家一樣。

言歸正轉,你是不是以為我今天要教你穿搭?不不不,這依然是一篇技術文章,想學穿搭女士學陳舒婷(《狂飆》中的大嫂),男士找陳舒婷那樣的女朋友就好了。筆者今天教你怎樣有“品味”的寫程式碼。



以下幾點可提升“品味”

說明:以下是筆者的經驗之談具有部分主觀性,不贊同的歡迎拍磚,要想體系化提升編碼功底建議讀《XX公司Java編碼規範》、《Effective Java》、《程式碼整潔之道》。以下幾點部分具有通用性,部分僅限於java語言,其它語言的同學繞過即可。

優雅防重

關於成體系的防重講解,筆者之後打算寫一篇文章介紹,今天只講一種優雅的方式:

如果你的業務場景滿足以下兩個條件:

1 業務介面重複呼叫的概率不是很高

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

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

如何監測到重複提交呢?MySQL唯一索引 + org.springframework.dao.DuplicateKeyException

程式碼如下:

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



用好lambda表示式

lambda表示式已經是一個老生常談的話題了,筆者認為,初級程式設計師向中級進階的必經之路就是攻克lambda表示式,lambda表示式和麵向物件程式設計是兩個程式設計理念,《架構整潔之道》裡曾提到有三種程式設計正規化,結構化程式設計(面向過程程式設計)、面向物件程式設計、函數語言程式設計。初次接觸lambda表示式肯定特別不適應,但如果熟悉以後你將開啟一個程式設計方式的新思路。本文不講lambda,只講如下例子:

比如你想把一個二維表資料進行分組,可採用以下一行程式碼實現

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



用好衛語句

各個大場的JAVA程式設計規範裡基本都有這條建議,但我見過的程式碼裡,把它用好的不多,衛語句對提升程式碼的可維護性有著很大的作用,想像一下,在一個10層if 縮排的接口裡找程式碼邏輯是一件多麼痛苦的事情,有人說,哪有10層的縮排啊,別說,筆者還真的在一個微服務裡的一個核心介面看到了這種程式碼,該介面被過多的人接手導致了這樣的局面。系統接手人過多以後,程式碼腐化的速度超出你的想像。

下面舉例說明:

沒有用衛語句的程式碼,很多層縮排

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

			}
		}else{
			
		}
	}else{
		
	}
}

使用了衛語句的程式碼,縮排很少

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

避免雙重迴圈

簡單說雙重迴圈會將程式碼邏輯的時間複雜度擴大至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);
	}
});

@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;
}

日誌列印避免只打整個引數

研發經常為了省事,直接將入參這樣列印

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

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

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

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

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

用異常捕獲替代方法引數傳遞

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

舉例:以下程式碼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();
}

自定義SpringBoot的Banner

別再讓你的Spring Boot啟動banner千篇一律,spring 支援自定義banner,該技能對業務功能實現沒任何卵用,但會給枯燥的程式設計生活新增一點樂趣。

以下是官方文件的說明: http://docs.spring.io/spring-boot/docs/1.3.8.RELEASE/reference/htmlsingle/#boot-features-banner

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

效果如下:

   _ _                   _                     _                 _       
  (_|_)_ __   __ _    __| | ___  _ __   __ _  | |__   ___   ___ | |_ ___ 
  | | | '_ \ / _` |  / _` |/ _ \| '_ \ / _` | | '_ \ / _ \ / _ \| __/ __|
  | | | | | | (_| | | (_| | (_) | | | | (_| | | |_) | (_) | (_) | |_\__ \
 _/ |_|_| |_|\__, |  \__,_|\___/|_| |_|\__, | |_.__/ \___/ \___/ \__|___/
|__/         |___/                     |___/                             

多用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);
}

利用鏈式程式設計

鏈式程式設計,也叫級聯式程式設計,呼叫物件的函式時返回一個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");
    }
}

未完,待續,歡迎評論區補充