我寫出這樣干凈的代碼,老板直夸我

一份整潔的代碼對于一個系統(tǒng)是多么重要。如果代碼寫的亂七八糟,最后的結果就是無法對這些代碼進行有效的管控。很有可能會毀掉這個系統(tǒng)。

什么才是整潔的代碼?

Biarne Stroustrup -【C++語言發(fā)明者,C++Programming Language(中譯版《C++程序設計語言》)一書作者】,我喜歡優(yōu)雅和高效的代碼。代碼邏輯應當直截了當,叫缺陷難以隱藏;盡量減少依賴關系,使之便于維護;依據(jù)某種分層戰(zhàn)略完善錯誤處理代碼;性能調至最優(yōu),省得引誘別人做沒規(guī)矩的優(yōu)化,搞出一堆混亂來。整潔的代碼只做好一件事。

有意義的命名

見名知意

命名要名副其實,雖然起個好名字要花時間,但省下來的時間比花掉的時間多。

變量、函數(shù)或類的名稱需要表達出:它為什么會存在,它做什么事,應該怎么用。如果這個名稱還需要注釋來補充,那就不算名不副實。

int?d;?//消逝的時間,以日計???...1

int?daysSinceCreation;???????//2

如上代碼,變量d什么也沒有說明。和后面的注釋八竿子打不著,第二行的代碼就清晰多了。

public?List?<int[]>?getThem()?{
????List?<?int[]?>?list1?=?new?ArrayList?<?int[]?>?();
????for?(int[]?x:?theList)?{
????????if?(x[0]?==?4)?{
????????????list1.add(x);
????????}
????}
????return?list1;
}

上面的代碼你或許有疑問:

  • (1) theList中是什么類型的東西?
  • (2) theList零下標條目的意義是什么?
  • (3)值4的意義是什么?
  • (4)我怎么使用返回的列表?
  • 可能當時人知道意思,但接手開發(fā)肯定會一臉懵逼的。

    如果對命名有困惑的,可以看看這個網(wǎng)站:https://unbug.github.io/codelf/


    輸入想要翻譯的中文,下面會列舉出「Github」上面使用過的相關命名。

    避免誤導

    比如你想定義一組賬號,不要用accountList,這樣會誤認為這是個List類型,除非真的是List類型的??梢允褂胊ccountGroup。

    再來看下面代碼:

    int?a=l;
    if(O==D)
    a=O1;?
    else
    l=o1;

    上面這串代碼整的傻傻分不清O和0,l和1。簡直亮瞎我的眼。

    有意義的區(qū)分

    public?static?void?copyChars(char?a1[],char?a2[])?{
    ?????....
    }

    參數(shù)過于混亂,改成

    public?static?void?copyChars(char?source[],char?destination[])?{
    ?????....
    }

    看著舒服多了。

    可搜索的名稱

    for(int?j?=?0;j?<?34;j++)?{
    ????s?+=?(t[j]?*?4)?/?5;
    }

    如圖:魔法值太多。可以給魔法值命名。

    private?static?final?int?WORK_DAYS_PRE_WEEEK?=?5?;
    private?static?final?int?NUMBER_OF_TASKS?=?34?;
    private?static?final?int?REAL_USE_DAYS?=?4?;
    privat?int?sum?=?0;

    for(int?j?=?sum;j?<?NUMBER_OF_TASKS;j++)?{
    ??? s +=?(t[j]?* REAL_USE_DAYS)?/ WORK_DAYS_PRE_WEEEK;
    }

    向上面這樣,至少可以搜索得到。

    類名與方法名

    類名應該是名詞短語。如:Student、Person、Account。

    方法名應該是動詞短語。如:getStudent、listPerson、save

    規(guī)范的方法

    短小精悍

    有些開發(fā)寫的方法內容上千行,這樣的方法估計連自己看著都累,為何不將內容作適當抽取呢。

    方法要短小。一般一個方法20行就足夠了。

    阿里巴巴要求一個方法總行數(shù)不能超過80行。

    只做一件事

    就是說每個方法只應該有一個功能,如果你要寫的方法功能較多,建議抽取,然后再組合。

    public?void?drawLottery()?{
    ???listUser();??//1.查詢用戶
    ???drawHandler();?//2.抽獎算法
    ???resultHandler();//3.抽獎結果處理
    }

    如上面代碼,將多個方法組合起來成一個方法清晰明了。如果把這3個功能全寫在drawLottery()。后面的開發(fā)來看,估計頭都要看禿。??

    使用描述性的名稱

    先來舉個栗子

    ?List<UpkeepConfig>?upkeepConfigs?=?upkeepConfigMapper.getAll(upkeepConfig);

    上面代碼getAll()一看以為是獲取所有的list,但是仔細看不是這個意思。我認為這樣命名比較合適:

    listByEntity(),這樣命名我很快就能知道:1.這個方法是返回list;2.這個方法是一個條件查詢;3.入?yún)⑹且粋€實體。

    別害怕長名稱。長而具有描述性的名稱,要比短而令人費解的名稱好。長而具有描述性的名稱,要比描述性的長注釋好。使用某種命名約定,讓函數(shù)名稱中的多個單詞容易閱讀,然后使用這些單詞給函數(shù)取個能說清其功用的名稱。

    方法參數(shù)

    最理想的參數(shù)數(shù)量是零(零參數(shù)函數(shù)),其次是一(單參數(shù)函數(shù)),再次是二(雙參數(shù)函數(shù)),應盡量避免三(三參數(shù)函數(shù))。有足夠特殊的理由才能用三個以上參數(shù)(多參數(shù)函數(shù))——所以無論如何也不要這么做。

    試想如果一個方法參數(shù)過長,也不利于其他開發(fā)者閱讀,不利于測試編寫測試用例。

    public?Object?getTransferTaskByCondition(HttpServletRequest?request,String?taskStatus
    ???,String?keyword,String?materialNumber,String?deviceCode
    ???,String?vehicleVinNumber,String?vehicleServiceDuty
    ???,String?arriDutyCell,String?equipElement,String?tenant,String?blDivisionCode
    ???,String?plateNumber,Integer?pageNum,Integer?pageSize,String?deviceTypeCode){
    ???
    ???...
    ???}
    ???

    上面這個方法的參數(shù)就問你怕不怕。

    上面代碼參數(shù)可以做適當封裝。

    public?Object?getTransferTaskByCondition(HttpServletRequest?request,TransferTask?transferTaskParam){
    ???
    ???...
    ???}

    如果函數(shù)看來需要兩個、三個或三個以上參數(shù),就說明其中一些參數(shù)應該封裝為類了。

    無副作用

    方法承諾只做一件事,但還是會做其他被藏起來的事。有時,它會對自己類中的變量做出未能預期的改動。有時,它會把變量搞成向方法傳遞的參數(shù)或是系統(tǒng)全局變量。無論哪種情況,都是具有破壞性的,會導致古怪的時序性耦合及順序依賴。

    public?class?UserValidator?{
    ????ptivate?Cryptographer?cryptographer;
    ????public?boolean?checkPassword(String?UserName,String?password){
    ????????User?user?=?UserService.getByName(userName);
    ????????if(user?!=?User.NULL){
    ????????????String?codedPhrase?=?user.getPassword();
    ????????????String?phrase?=?cryptographer.decrypt(codedPhrase?,password);
    ????????????if?("valid?Password"equals(phrase)){
    ????????????????Session.initialize();
    ????????????????return?true;?
    ????????????}
    ????????????return?false;
    ????????}
    ????}

    如上面代碼,反方法名checkPassword以為就是一個密碼校驗。但是看方法有一個Session初始化。該名稱并未暗示它會初始化該次會話。所以,當某個誤信方法名的調用者想要檢查用戶有效性時,就得冒抹除現(xiàn)有會話數(shù)據(jù)的風險。

    分隔指令與詢問

    方法要么做什么事,要么回答什么事。方法應該修改某對象的狀態(tài),或是返回該對象的有關信息。兩樣都干常會導致混亂。看看下面的例子:

    public?boolean?set(String?attribute,?String?value);

    這個方法我們知道,設置某個屬性成功返回true,否則返回false。

    但如果這樣

    if(set("userName","lvshen")){
    ??....
    }

    其他開發(fā)閱讀這段代碼時,會有疑問,這是在表達 username屬性值是否之前已設置為 lvshen嗎?或者它是在表達username屬性值是否成功設置為 lvshen呢?從這行調用很難判斷其含義,因為set看不清是動詞還是形容詞。

    這時好的解決方案是:

    if?(attributeExists("username")){
    ??setAttribute("username","lvshen");
    }

    抽離try/catch代碼塊

    建議將try和catch代碼塊的主體部分抽離出來,如下

    public?void?delete(Page?page)?{
    ??try{
    ???deletePageAndAllReferences(page);
    ??}catch(Exception?e){
    ???logErrr(e);
    ??}
    }

    private?void?deletePageAndAllReferences(Page?page)?throws?Exception?{
    ?...
    }

    另外不要對大段代碼進行try/catch,這樣不利于定位問題。

    行動起來

    下面這段話摘至《Clean Code》作者:

    ?

    我寫函數(shù)時,一開始都冗長而復雜。有太多縮進和嵌套循環(huán)。有過長的參數(shù)列表。名稱是隨意取的,也會有重復的代碼。不過我會配上一套單元測試,覆蓋每行丑陋的代碼。

    然后我打磨這些代碼,分解函數(shù)、修改名稱、消除重復。我縮短和重新安置方法有時我還拆散類。同時保持測試通過。

    最后,遵循本章列出的規(guī)則,我組裝好這些函數(shù)我并不從一開始就按照規(guī)則寫函數(shù)。我想沒人做得到

    ?

    就像寫作文一樣,好的代碼也不是一次性寫出來的,需要反復琢磨。

    必要和不必要的注釋

    無用的注釋

    糟糕的代碼才寫注釋,如果能用代碼表達,為何還要加注釋呢。

    良好的注釋能夠提高代碼的閱讀效率。然而亂七八糟的注釋有可能會搞壞這個功能。

    注釋會撒謊。也不是說總是如此或有意如此,但出現(xiàn)得實在太頻繁。注釋存在的時間越久,就離其所描述的代碼越遠,理解起來就很容易錯誤。原因很簡單。程序員不能堅持維護注釋。

    要知道注釋也不能美化糟糕的代碼,所以花點時間好好重構下代碼吧。

    有用的注釋

    當然有些注釋也是必要的。比如待開發(fā)的「TODO」注釋,API的Javadoc注釋。

    廢話注釋

    /**
    ??*默認構造函數(shù)
    ??*/
    ??protected?AnnualDateRule();
    ??/**
    ????*每月天數(shù)
    ????*/
    ??private?int?dayofMonth;
    /**
    ??*
    ??*@return?每月天數(shù)
    ??*/
    public?int?getDayOfMonth(){
    ????return?dayofMonth;
    }

    像上面這種注釋就感覺是廢話了。

    注釋掉的代碼

    不用的代碼要不刪掉,要不注釋說明不要刪。如果注釋了大段代碼,又不做任何說明,其他人看見了也不敢刪掉,或者本來是還有用的代碼被誤刪了。

    這樣導致注釋掉的代碼堆積在一起,越來越臃腫。

    格式

    代碼順序

    若某個方法調用了另外一個,就應該把它們放到一起,而且調用者應該盡可能放在被調用者上面。這樣,程序就有個自然的順序。若堅定地遵循這條約定,讀者將能夠確信方法聲明總會在其調用后很快出現(xiàn)。這樣極大的增強了整個模塊的可閱讀性。

    public?void?funA()?{
    ??funB();
    ??funC();
    }

    public?void?funB(){
    ?...
    }
    public?void?funC(){
    ?...
    }

    當然一個開發(fā)團隊應該有自己固定的格式規(guī)則。開發(fā)遵循規(guī)則就可以了。

    別返回null值

    假設有著一段代碼:

    List<Student>?students?=?getStudents();
    if(students?!=?null)?{
    ???students.forEach(student?->?{
    ?????student.setName(name);
    ???});
    }

    這里有非空判斷,是因為getStudents()有返回null的情況。如果該方法修改為返回空list(建議返回不可變集合ImmutableList.of()),就少了if判斷,何樂而不為。

    List<Student>?students?=?getStudents();
    students.forEach(student?->?{
    ?student.setName(name);
    });

    必要的單元測試

    對于系統(tǒng)的核心功能,一定要有單元測試,單元測試有利于提高系統(tǒng)健壯性。而且有利于重復測試。這樣比用swagger方便的多。而且其他程序員也可以測試該方法并了解其功能。

    當然,測試代碼也需要干凈整潔。不易讀懂,混亂的測試代碼等同于沒有測試。

    類應該短小,建議不要超過500行。

    當然你可能害怕數(shù)量巨大的短小的類會讓人一難以下子一目了然抓住全局。

    這就好比:你是想把工具歸置到有許多抽屜、每個抽屜中裝有定義和標記良好的組件的工具箱中呢,還是想要少數(shù)幾個能隨便把所有東西扔進去的屜?


    近5000行的類就問怕不怕。

    逐步改進

    系統(tǒng)需要要迭進,在迭進過程中生成干凈整潔的代碼。這里涉及到重構代碼,去除重復性代碼。

    關于重構,你可以特意留意命名方式,函數(shù)大小,代碼格式。

    ?

    代碼能工作還不夠。能工作的代碼經常會嚴重崩潰。滿足于僅僅讓代碼能工作的程序員不夠專業(yè)。他們會害怕沒時間改進代碼的結構和設計,我不這么認為。沒什么能比糟糕的代碼給開發(fā)項目帶來更深遠和長期的損害了。進度可以重訂,需求可以重新定義,團隊動態(tài)可以修正。但糟糕的代碼只是一直腐敗發(fā)酵,無情地拖著團隊的后腿。我無數(shù)次看到開發(fā)團隊蹣跚前行,只因為他們匆。? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??——來自《Clean Code》

    ?

    關于自己編碼的一些經驗

    for循環(huán)

    或許你會經經常寫下面的代碼:

    students.forEach(?stu?->?{
    ??...
    ???xxxMapper.getById(stu.getId());??//數(shù)據(jù)庫查詢
    ??...
    });

    如果上面的students數(shù)量不可控,那么for循環(huán)次數(shù)也就不可控。就會有未知的數(shù)據(jù)庫查詢次數(shù)。如果有1000個學生,那么一個用戶調用這里查1000次數(shù)據(jù)庫,1000個用戶調用這里查 次,在并發(fā)場景下對數(shù)據(jù)庫壓力有多大,想想都可怕。

    建議這么寫:

    //先一次批量查詢
    List<Student>?sts?=?xxxMapper.listByIds(ids);
    //然后轉換成Map
    Map<String,List<Student>>?stuMap?=?sts.stream().collect(Collectors.groupingBy(Student::Id));

    students.forEach(?stu?->?{
    ????//通過map獲取
    ???stuMap.get(stu.getId());?
    });

    更新

    來看這段代碼:

    SourceDetail?update?=?sourceDetailService.getById(id);
    update.setXXX(xxx);
    ...
    sourceDetailService.updateNotNull(update);

    updateNotNull實際上就是通過主鍵更新,這里知道了主鍵就沒必要先查一次庫了。可以這樣做:

    SourceDetail?update?=?new?SourceDetail();
    update.setId(xxx);
    ...
    sourceDetailService.updateNotNull(update);

    內存節(jié)省

    Arrays.asList(strArray)返回值是仍然是一個可變的集合,但是返回值是其內部類,不具有add方法,可以通過set方法進行增加值,默認長度是「10」。

    Collections.singletonList()返回的同樣是不可變的集合,但是這個長度的集合只有「1」,可以減少內存空間。但是返回的值依然是Collections的內部實現(xiàn)類,同樣沒有add的方法,調用add,set方法會報錯。

    別用Random生成隨機數(shù)

    由于java.util.Random類依賴于偽隨機數(shù)生成器,因此該類和相關的java.lang.Math.random()方法不應用于安全關鍵應用程序或保護敏感數(shù)據(jù)。在這種情況下,應該使用依賴于加密強隨機數(shù)生成器(RNG)的java.security.SecureRandom類。

    「PRNG(偽隨機數(shù)):」偽隨機數(shù), 計算機不能生成真正的隨機數(shù),而是通用一定的方法來模擬隨機數(shù)。偽隨機數(shù)有一部分遵守一定的規(guī)律,另一部分不遵守任何規(guī)律。

    「RNG(隨機數(shù)):」隨機數(shù)是由“隨機種子”產生的,“隨機種子”是一個無符號整形數(shù)。

    //反例:
    Random?random?=?new?Random();
    byte?bytes[]?=?new?byte[20];
    random.nextBytes(bytes);
    //正例:
    SecureRandom?random?=?new?SecureRandom();?
    byte?bytes[]?=?new?byte[20];
    random.nextBytes(bytes);

    如果再多線程情況下,建議用ThreadLocalRandom。

    ThreadLocalRandom相對于Random可以減少多線程資源競爭,保證了線程的安全性。public class ThreadLocalRandom extends Random因為構造器是默認訪問權限,只能在java.util包中創(chuàng)建對象,故提供了一個方法ThreadLocalRandom.current()用于返回當前類的對象。

    善用Java8 API

    還是舉例子,如果你要計算兩個日期的時間差。你可能會這樣做:

    Calendar?bef?=?Calendar.getInstance();
    ??Calendar?aft?=?Calendar.getInstance();
    ??bef.setTime(before);
    ??aft.setTime(after);
    ??long?result=?(aft.getTimeInMillis()-bef.getTimeInMillis())/(1000*3600*24);

    這里我建議使用「Java8」的日期類:

    long?diffMinutes?=?ChronoUnit.MINUTES.between(Instant.now(),?sendDate.toInstant());

    ChronoUnit擁有不可變和線程安全性,而Calendar用作共享變量本身沒有線程安全控制的。同樣Instant也是不可變對象。所以嘗試使用Java8的日期時間類吧。

    不要怕麻煩,寫完代碼后,請花點時間,優(yōu)化下自己的代碼,并養(yǎng)成習慣。

    這是對自己負責,也是對系統(tǒng)負責。

    最后

    關注公眾號「Lvshen_9」,后臺回復"「手冊」",獲取阿里巴巴Java開發(fā)手冊最新版。

    最后編輯于
    ?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
    【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
    平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

    友情鏈接更多精彩內容