天天看點

Java進階之路-如何寫出高品質的代碼——《我的Java打怪日記》前言最重要的是可讀性高品質和低品質代碼的特點如何寫出高品質代碼寫在最後開源架構和規範Java服務端最佳實踐參考

前言

俗話說好看的皮囊千篇一律,當然高品質的代碼我們也是有目共睹,對于看過spring、springboot、tomcat等底層代碼的同學來說,由衷的感歎:這代碼寫的真TMD好。那對于我們程式員而言,那如何才能寫出高品質的代碼呢?讓别人看到我們代碼的時候也由衷的說一句:這代碼寫的真TMD好。更進一步我們如何做才能寫出高品質的代碼?本文主要是分享下作者在探索高品質代碼之路上的一些個人看法,供同樣奮鬥在代碼之路的同學參考。

最重要的是可讀性

Java進階之路-如何寫出高品質的代碼——《我的Java打怪日記》前言最重要的是可讀性高品質和低品質代碼的特點如何寫出高品質代碼寫在最後開源架構和規範Java服務端最佳實踐參考

正如Martin Fowler(重構那本書的作者,軟體界的泰鬥)所說:

任何一個傻瓜都能寫出計算機可以了解的代碼。唯有寫出人類容易了解的代碼,才是優秀的程式員。

代碼是需要維護的,而維護的前提首先是看懂代碼,這就要求代碼具有可讀性。可讀性高的代碼能夠降低後續的維護成本,提升後續開發的效率。

項目代碼是需要維護的,除非你寫的是一次性代碼

大部分情況下是“别人”維護代碼,而不是代碼的第一作者

網際網路疊代節奏快,人員更新換代頻繁,需求日新月異,導緻代碼維護頻率高

建構項目需要大量的代碼,項目的規模動辄百萬行代碼量,甚至千萬行代碼

代碼可讀性我們一般是指源代碼(source code)具有可讀性,源代碼的目标使用者有兩類,一類是編譯器,如JDK編譯器,一類是後續維護的人,對于第一類使用者來說,代碼可讀性的要求沒有那麼高,隻要程式能編譯通過,可以正常運作就行。對于第二類使用者來說,代碼追求可讀性的目的是降低他人閱讀你的代碼的難度,可讀性高的代碼就像一篇字型工整、段落清晰、邏輯明确的文章,讓看到文章的人願意繼續讀你的文章,經曆過聯考的人可以想象一下閱卷老師看到聯考作文的感受。

綜上,高品質代碼最重要的是可讀性。什麼高内聚低耦合、可擴充性是高品質代碼具有的特點,但可讀性才是高品質代碼最根本的要求。

高品質和低品質代碼的特點

好代碼的特性

(可讀性高)編碼标準統一,風格一緻,别人願意維護代碼!

先把阿裡巴巴編碼手冊考了吧,Java體系下的行業标準。整體标準的統一,風格一緻,可以避免很多不必要的坑,也在代碼閱讀上形成基本的共識。

(擴充性強)高内聚低耦合

"低耦合, 高内聚"是由于人腦自身的限制所決定的

  1. 人的記憶是短暫而非永久的
  2. 在同一時間, 人能考慮到的因素是有限的

由于以上2個限制,是以人必須把複雜的東西分割成不同的元件,其中元件内的概念盡量貼近以至于思考一個因素很容易能聯想或者推斷另外一個,或者說這些東西在一起很容易記憶、分析、推理... 等, 這就是高聚合。而在這個思考過程中,如果可以盡量把“不相幹”的東西排出在外,那麼人腦的負擔就會小很多,這就是低耦合。

好的接口應當滿足設計模式六大原則,很多設計模式、架構都是基于高内聚低耦合這個出發點的:

  • 開閉原則:對擴充開發,修改關閉
  • 迪米特原則:最少知道原則,間接朋友不要依賴
  • 裡氏替換原則:依賴父類的地方可替換成子類,子類不要重寫父類的方法
  • 職責單一原則:類的職責單一
  • 接口隔離:接口決定可使用的範圍
  • 依賴倒置原則:細節依賴抽象,面向接口程式設計

代碼壞味道

你願意維護代碼嗎?

Martin Fowler的

重構 - 改善既有代碼的設計(Refactoring: Improving the Design of Existing Code)

和Robert C.Martin的

代碼整潔之道(Clean Code)

關于代碼壞味道都有比較詳細的介紹,尤其是重構這本書,指出二十多種有“壞味道”的代碼,從定性的角度出發給出了界定代碼壞味道的方法,當然Martin Fowler說的都是真真切切的道理,真理性毋庸置疑。

本文的作者在實踐過程中發現,主觀的角度更能說明代碼是有壞味道的,這也是依據高品質代碼最重要是可讀性,那何不問問将要接手你代碼人的感受?這也是從真正的使用者的回報出發(代碼是别人維護的,那我們讓别人來評價我們的代碼壞味道。

如果别人問你:“這個代碼交給你,你願意維護嗎?”如果你的回答是肯定不願意,那這個代碼壞味道就比較明顯。

ELSE IF語句多層嵌套

多層嵌套的IF ELSE語句,尤其是哪些嵌套7~8層的IF ELSE語句,對于維護的人來說簡直是地獄。如果你體感不明顯,請看下下圖,對于多層嵌套的IF ELSE語句,維護代碼的人很難找到正确的路徑最終實作正确修改代碼。

Java進階之路-如何寫出高品質的代碼——《我的Java打怪日記》前言最重要的是可讀性高品質和低品質代碼的特點如何寫出高品質代碼寫在最後開源架構和規範Java服務端最佳實踐參考

圖檔來自《What Is Bad Code, Why It Is Dangerous, and How to Write Good Code》

代碼風格各異

代碼風格本身無好壞之分,就像你是DDD架構風格,還是經典的MVC三層架構風格,本身架構風格沒有好壞之分,隻是大家對于同一個事物從不同的側面認知不同,隻要團隊内的風格一緻就好。我覺得這特别像“靠右行駛通行制”,這也是道路交通規則中最基本的原則。如果人、車在道路上随意行動,必然會導緻交通混亂、毫無秩序,甚至碰撞、車禍不斷。

代碼風格各異,就像一群人到一起,每個人都說自己國家的語言,然後大家彼此都沒法了解。

Martin Fowler曾經在一篇文章中曾經引用過Phil Karlton的話:There are only two hard things in Computer Science: cache invalidation and naming things.

在 CS 領域中,有兩件事是非常難的,一個是緩存失效,一個是命名。可見命名規範統一是多麼的重要。

Java進階之路-如何寫出高品質的代碼——《我的Java打怪日記》前言最重要的是可讀性高品質和低品質代碼的特點如何寫出高品質代碼寫在最後開源架構和規範Java服務端最佳實踐參考

如何寫出高品質代碼

關于如何寫出高品質代碼,本文從戰略上給出一些自己的思考和建議,并不是具體的方法,希望給前進路上的你一些啟發。

代碼是臉面

在實際的工作中,我一直堅持“代碼是臉面”的理念,當然也在團隊中經常強調。代碼是臉面是說,在寫代碼的時候盡量别依靠自己的思維慣性去寫代碼,思維慣性的下意識是:如果怎麼怎麼樣,我就怎麼怎麼樣,如果再怎麼怎麼樣,我就再怎麼怎麼樣,這樣寫出來的代碼無形中就是各種IF ELSE的嵌套。

代碼是臉面是說:寫代碼的時候時刻懷着敬畏之心寫代碼,時刻想着我的代碼代表着我的臉面,代表着一個人的口碑,對待代碼我們要從心态上戰略擺正。這樣才能認真的對待自己寫的每一行代碼,這樣才會精雕細琢自己的每一行代碼,防止無意識的代碼壞味道。常言道:君子有三畏:常懷敬畏之心,方能行有所止。

别把事情複雜化,即控制複雜性

别把事情複雜化主要是指:異常處理盡量别複雜化、方案設計盡量别複雜化。

異常和錯誤處理是造成軟體複雜的罪魁禍首之一。如果所有的異常都考慮到,那程式則無法繼續下去,這會導緻過度防禦性的程式設計,造成代碼的可讀性很差,而且也會把很多錯誤掩蓋。作者比較推薦在實際的項目中先把主體功能邏輯實作,異常先簡化處理,後邊再逐漸的優化異常處理,不能從一開始就考慮過多的異常,導緻程式無法繼續寫下去。

方案設計盡量别複雜化,别想着從一開始就給出完美的設計方案,這樣隻會讓代碼變更複雜,有點“殺雞焉用牛刀”的味道,無形中造成非必要的複雜性。

鼓勵抄代碼

團隊内鼓勵抄代碼的幾點考慮:

  • 優秀的代碼可以很快得到普及和傳播,這樣是最快提高團隊代碼品質的方法
  • 可以拉齊團隊内的代碼風格、規範和設計,抄代碼讓團隊内的代碼風格趨于一緻

代碼是疊代出來的

網際網路的快速發展,很難一開始就做出完美的設計,寫出高品質的代碼,隻能持續的投入精力優化和重構代碼,才能在追求高品質代碼的路上更進一步。好的代碼是日拱一卒的結果,在日常工作中要重視代碼和設計細節的改進。

唯手熟爾

通往高品質代碼的捷徑隻有一條:唯手熟爾,即經常寫代碼。之前作者一直有個誤區,認為多看優秀的代碼(Tomcat、spring等)就能提高自己的代碼水準,後來發現紙上得來終覺淺,絕知此事要躬行,慢慢認知到自己的錯誤之後,後來實作同一個功能實作,會采用各種方式寫一遍,設計模式也是變換了多種,最終終于對設計模式有了自己的了解,代碼水準才慢慢的有了明顯的提高。

複雜性轉移,站在别人的肩膀上開發代碼

軟體工程發展到今天,各種開源架構層出不窮,各種針對特定領域的軟體也各種各樣,我們在解決問題的時候,可以考慮利用現有已有的成果和技術,如:作者最近有個項目,項目中涉及到拓撲結構圖展示,開始我們是采用普通的MYSQL資料存儲拓撲資料,造成查詢拓撲圖的時候代碼很複雜,後來通過引入阿裡雲上的圖資料庫GDB,讓原來複雜的邏輯處理變成隻有幾行代碼實作。再舉個例子:現在咱們會直接寫原生的servlet程式嗎?當然大家的回答是不會,甚至很多同學聽不沒聽過servlet程式,現在大家都基于springboot開發服務端程式,簡單寫個controller加幾個注解,就能很容易的開發服務端程式。

Java技術發展到今天,基本在各個領域都有專業的技術人員在投入建設,我們應該站在他們的肩膀上開發代碼,遇到事情要做充分的技術調研,再開發着手幹,這樣讓很多本來需要我們處理的複雜性代碼轉移出去,另外也讓程式看起來更簡潔,可讀性更高,作者在最後推薦幾個經常用的開源架構。

忌造不圓的輪子

忌造不圓的輪子是說在解決問題的時候,盡量别自己獨自蠻幹,多上google查現有哪些輪子已經有了,哪些輪子解決了哪些問題,選擇合适的輪子解決自己的問題。在現有的輪子基礎上開發,别重複造一個同樣的輪子,且我們造的輪子還不圓。

讓IDE幫你寫代碼

用IDEA寫代碼,寫的過程中IDE會有各種提示,是什麼問題,應該怎麼解決,其實IDE已經幫我做了很多事,我們應該充分利用IDE的能力,寫出高品質的代碼。

IDE就像一個聰明的老師,一直在看我們寫的代碼,還把怎麼優化代碼都告訴我們了。

Java進階之路-如何寫出高品質的代碼——《我的Java打怪日記》前言最重要的是可讀性高品質和低品質代碼的特點如何寫出高品質代碼寫在最後開源架構和規範Java服務端最佳實踐參考

寫在最後

寫高品質代碼的方法論其實有很多種,其中問題的關鍵還是多實踐、多動手、多琢磨、多看、再多實踐,通過不斷的循環,代碼寫多了,通過學習和實踐自然慢慢的代碼的品質就提升了。

開源架構和規範

阿裡巴巴開發手冊

spring全家桶(springboot、springframework)

工具類guava和apache common

Java時間處理神器jodd

簡化bean代碼神器lombok

bean對象轉化利器mapstruct

并發架構disruptor

Java服務端最佳實踐參考

異常的統一處理

@RestController
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 預設的其他錯誤資訊封裝
     *
     * @param req req
     * @param ex exception
     * @return response
     */
    @ExceptionHandler(value = {BaseException.class})
    @ResponseBody
    public ResponseResult customHandler(HttpServletRequest req, BaseException ex) {
        // 這裡可以對異常統一進行收集處理
        log.error(ExceptionUtil.buildErrorMessage(ex));
        return ResponseResult.createResult(ex.getErrorCode(), ex.getErrorMsg());
    }
    /**
     * 其他未處理的異常
     *
     * @param e exception
     * @return response
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseResult noHandlerFoundException(Exception e) {
        log.error(ExceptionUtil.buildErrorMessage(e));
        return new ResponseResult(ResultStatus.ERROR_SERVER_INTERNAL);
    }
    /**
     * 請求參數缺少異常捕獲
     *
     * @param e exception
     * @return response
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseBody
    public ResponseResult missRequestParamException(MissingServletRequestParameterException e) {
        log.error(ExceptionUtil.buildErrorMessage(e));
        return new ResponseResult(ResultStatus.ERROR_PARAM);
    }
    /**
     * 請求參數缺少異常捕獲
     *
     * @param e exception
     * @return response
     */
    @ExceptionHandler(ValidationException.class)
    @ResponseBody
    public ResponseResult missRequestParamException(ValidationException e) {
        log.error(ExceptionUtil.buildErrorMessage(e));
        return new ResponseResult(ResultStatus.ERROR_PARAM, e.getMessage());
    }
    @ExceptionHandler(UnknownUserException.class)
    @ResponseBody
    public ResponseResult unknownUserException(UnknownUserException e) {
        e.printStackTrace();
        log.error(ExceptionUtil.buildErrorMessage(e));
        return new ResponseResult(ResultStatus.ERROR_USER_QUERY_FAIL, e.getMessage());
    }
}      

響應結果和錯誤碼

@Data
public class ResponseResult<T> {
    private T data;
    private int errorCode;
    private String errorMsg;
    public ResponseResult(ResultStatus status, T content) {
        this.data = content;
        this.errorCode = status.getValue();
        this.errorMsg = status.getMessage();
    }
    public ResponseResult(ResultStatus status) {
        this.errorCode = status.getValue();
        this.errorMsg = status.getMessage();
    }
    public ResponseResult(T content, int errCode, String errMsg) {
        this.data = content;
        this.errorCode = errCode;
        this.errorMsg = errMsg;
    }
    public static <T> ResponseResult<T> createResult() {
        return new ResponseResult(null, 0, "success");
    }
    public static <T> ResponseResult<T> createResult(T content) {
        return new ResponseResult(content, 0, "success");
    }
    public static <T> ResponseResult<T> createResult(int errCode, String errMsg) {
        return new ResponseResult(null, errCode, errMsg);
    }
    public static <T> ResponseResult<T> createResult(ResultStatus status, T data) {
        return new ResponseResult(status, data);
    }
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("errCode=").append(errorCode);
        sb.append("errMsg=").append(errorMsg);
        sb.append("data=").append(data);
        return sb.toString();
    }
}      
public enum ResultStatus {
    /**
     * 請求成功
     */
    SUCCESS(0, "success"),
    /**
     * 請求失敗
     */
    FAILURE(-1, " failure"),
    ERROR_LOGGED_INVALID(-1, "登入失效"),
    ERROR_PARAM(4001, "參數錯誤"),
    ERROR_SERVER_INTERNAL(5000, "伺服器内部錯誤");
    private final int value;
    private final String message;
    ResultStatus(int value, String message) {
        this.value = value;
        this.message = message;
    }
    public static ResultStatus parse(int value) {
        ResultStatus retValue = ResultStatus.SUCCESS;
        for (ResultStatus item : ResultStatus.values()) {
            if (value == item.getValue()) {
                retValue = item;
                break;
            }
        }
        return retValue;
    }
    public int getValue() {
        return this.value;
    }
    public String getMessage() {
        return this.message;
    }
}      

JSON格式資料統一轉換

@Configuration
public class FastJsonConvert {
    @Bean
    public HttpMessageConverters fastJsonHttpMessageConverters() {
        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat,
                SerializerFeature.WriteNullStringAsEmpty,
                SerializerFeature.WriteNullNumberAsZero,
                SerializerFeature.WriteNullListAsEmpty,
                SerializerFeature.WriteMapNullValue);
        SerializeConfig config = new SerializeConfig();
        config.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
        fastJsonConfig.setSerializeConfig(config);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        HttpMessageConverter<?> converter = fastConverter;
        return new HttpMessageConverters(converter);
    }
}      

bean的mapper映射,簡化bean的互相轉化

@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface AoneMapper {
    AoneMapper INSTANCE = Mappers.getMapper(AoneMapper.class);
    AoneDeployDetailDTO entityToDto(AlibabaAoneEnvGetdeploybyidResponse.DeployObj object);
    @Mapping(source = "objectDO.processId", target = "processId")
    @Mapping(expression = "java(objectDO.getReleases().get(0).getReleaseRevision())", target = "releaseReversion")
    @Mapping(expression = "java(objectDO.getReleases().get(0).getReleaseUrl())", target = "releaseUrl")
    AoneBranchInfoDTO entityToDto(ObjectDO objectDO);
}      

Lombok注解類,簡化bean定義實作

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AppInfo {
    private int itrackAppId;
    private String aoneAppName;
    private boolean deployByAone;
    private int aoneAppId;
    private Map<String, List<String>> appGroupNameAndIpListMap;
}