寫了10年JAVA程式碼,為何還是給人一種亂糟糟的感覺?

風平浪靜如碼發表於2019-12-02

接觸過不少號稱寫了10多年程式碼的程式設計師,可經常還是會發現他們的程式碼給人一種亂糟糟的感覺,那麼如何才能寫出讓同事感覺不那麼亂的程式碼呢?

一、為什麼要寫這篇文章

在開篇之前先說明下為什麼要寫這篇文章?在Java的世界裡MVC軟體架構模式絕對是經典的存在(PS:MVC是一種軟體架構方式並不只有Java有),如果你是在最近十年前後進入Java的程式設計世界,那麼你會發現自己這些年似乎從來沒有逃離MVC架構模式的牢籠,只不過換著使用了不同的MVC框架,如早期的Struts1、Struts2以及現在幾乎一統江湖的Spring MVC(少數自行封裝MVC框架的公司除外)。

而隨著網際網路技術的發展,特別是Ajax等富客戶端技術的發展,前端技術逐步形成了一套體系,並且逐步從後端程式碼(如JSP)中剝離出來,從而形成了現在普遍流行的前後端分離模式(這也是一段時間內為什麼前端工程師會出現大量需求的原因),而這也對傳統的MVC模式產生了一點小的改變,因為現在基於Java的後端服務中很少會有大量處理複雜介面邏輯的程式碼出現,因此MVC中的V(View)這一層就逐步被各類前端技術所替代,如AngularJS、React等。

所以現在的Java服務端絕大部分情況下只是在處理M(Model)+C(Controller)的邏輯,而從概念上來看,好像Model代表的就是資料模型、而C則是一種控制層邏輯,所以很多人(甚至包括一些寫了很多年Java程式碼的人)有時候都會被這個概念所迷惑而在Model和Controller層之間搖擺不定,在這裡我們需要明確MVC模式中的M不僅僅代表的是資料模型,而是包括了資料模型之內的所有業務邏輯相關的程式碼,而C則是比較輕的,它被賦予只有處理輸入/輸出引數以及對該請求進行邏輯流程控制的職能,如果你的程式碼中對Controller層有過重的邏輯程式碼侵入,要知道這是不符合MVC架構規範的!

在MVC架構定義中,由於M代表了所有業務邏輯相關的程式碼,所以M是要重點設計和規範的,其程式碼的結構和規範直接決定了軟體的可維護性及質量,從本質上來說就是如何進行"程式碼結構+軟體設計原則+設計模式"的組合運用。當然上面只是一句話,而其內涵則是一件非常考驗程式設計水平的事情。關於軟體設計原則+設計模式的內容非常豐富也需要時間+經驗的積累!而程式碼結構則是可以通過一定規範進行約定,結合Spring MVC框架至少我們可以寫出層次結構儘可能一致的程式碼!

二、應用分層怎麼搞?

事實上關於Java如何規範開發的問題,不同公司的規範略有不同,不過作為國內Java語言應用最為廣泛的公司——阿里巴巴釋出的《阿里巴巴Java開發手冊》中對應用的分層結構已經做了比較合理的劃分!這裡作者並不想標新立異,只是在此基礎上做更為詳細的解釋和說明從而讓使用Spring MVC框架的同學能夠更好地明確其分層的對應關係!

分層結構

以下分層結構基於Spring MVC框架,總體上與阿里巴巴開發手冊應用分層方式一致,分層結構示意圖如下:

寫了10年JAVA程式碼,為何還是給人一種亂糟糟的感覺?

在基於Spring MVC框架的開發中,Controller層作為服務的入口主要承擔接收和轉換由終端層或者其他服務傳送的網路請求,並將其轉化為Java資料物件,然後對資料物件進行引數合法性校驗(如欄位長度、型別、數值的合法性等等)。之後通過在Controller依賴注入對應Service層服務介面,並進行業務邏輯層方法呼叫,如果業務邏輯並不複雜(是否複雜判斷標準可通過方法程式碼行數、條件邏輯複雜度以及站在旁者角度看看是否便於維護等指標進行判斷)那麼可以直接運算元據庫持久層完成業務邏輯;而如果Service層方法寫著寫著發現非常的多,邏輯條件也比較多,並且每個條件所需要處理的程式碼量超過一定的規模,那麼此時你就要考慮是否需要要對該方法進行優化了!

而關於優化的方式依據邏輯的複雜程度可以做不同等級的拆分,例如簡單點可以拆分一個私有方法處理該方法中的某一部分邏輯,從而減少主業務方法的程式碼量。而如果該業務層方法後面對應的是一個龐大的邏輯,例如在交易支付系統中,Controller層定義了一個支付的入口服務,而進入Service層方法後根據不同的業務接入方、不同的支付方式及支付渠道,都需要進行大量不同邏輯的處理,那麼此時就需要考慮對這些不同場景的業務邏輯進行類級別的拆分,如通過工廠模式拆分不同的支付渠道處理類邏輯,而對於公共的處理邏輯則可以通過抽象類定義抽象方法進行抽象。例如私有方法拆分程式碼示例:

@Override
public SearchCouponNameBO searchCouponNameList(SearchCouponNameDTO searchCouponNameDTO) {
    SearchCouponNameBO searchCouponNameBO = SearchCouponNameBO.builder().total(0).build();
    SearchResult searchResult;
    try {
        BoolQueryCondition boolQueryCondition = searchCouponNameListConditionBuild(searchCouponNameDTO);
        SearchBuilderConstructor searchBuilderConstructor = new SearchBuilderConstructor(boolQueryCondition);
        searchBuilderConstructor.addFieldSort("id", SortOrderEnum.DESC);
        searchBuilderConstructor.setFrom(searchCouponNameDTO.getOffset());
        searchBuilderConstructor.setSize(searchCouponNameDTO.getLimit());
        searchResult = salesCouponEsMapper.selectCouponNameByCondition(searchBuilderConstructor);
    } catch (Exception e) {
        throw new SalesCouponNameException(SalesCouponNameErrorCode.COUPON_NAME_ES_QUERY_ERROR.getCode(),
                SalesCouponNameErrorCode.COUPON_NAME_ES_QUERY_ERROR.getMessage(),
                searchCouponNameDTO);
    }
    if (searchResult != null && searchResult.getHits().getHits().length > 0) {
        List<Integer> idList = getIdListFromEsSearchResult(searchResult);
        List<SalesCouponNamePO> salesCouponNamePOList = salesCouponNameMapper.selectByIdList(idList);
        List<SalesCouponNameBO> couponNameBOList = SalesCouponNameConvert.INSTANCE
                .convertCouponNameBOList(salesCouponNamePOList);
        searchCouponNameBO.setList(couponNameBOList);
        searchCouponNameBO.setTotal((int) searchResult.getTotalHits());
    }
    return searchCouponNameBO;
}
複製程式碼

在該Service入口方法中,需要根據從ES查詢的分頁ID去真實的MySQL中進行資料獲取(ES資料儲存不全,只是為了進行優化效能將分頁邏輯放入ES),而在處理ES資料時,需要從ES資料結果集中抽象ID列表,對於這部分邏輯出於程式碼量的考慮,這裡我們抽象一個Service層私有方法,如:

private List<Integer> getIdListFromEsSearchResult(SearchResult searchResult) {
    SearchHit[] searchHits = searchResult.getHits().getHits();
    List<Integer> idList = Arrays.asList(searchHits).stream().map(SearchHit::getSourceAsMap)
            .map(o -> Integer.parseInt(String.valueOf(o.get("id"))))
            .collect(Collectors.toList());
    return idList;
}
複製程式碼

以上程式碼示例,本質上是一種最簡單的方法抽象(別的語言叫函式),如果在程式碼量略大,但是邏輯本身複雜度還不是特別高的情況下,這種方式是最常用的!也是在你不知道怎麼拆分,讓程式碼不那麼難以維護的一種非常有效的手段。

而工廠+責任鏈等也是業務層拆分常用的手段,此時需要基於Service層業務入口方法進行程式碼結構的二次拆分,在分層結構上這部分介於Service層和Dao層之間的程式碼稱之為通用業務處理層(Manager)。關於這部分由於可以發揮空間非常大,很難有一套標準的答案,但作為一名優秀的程式設計者要時刻有抽象的思維,不管拆分得是否足夠合理,至少要讓你的程式碼不至於過於臃腫!這裡我們將Service層拆分層次定義為以下三個等級:

  • 等級1:私有方法拆分;
  • 等級2:工廠+責任鏈運用(有效的類的拆分);
  • 等級3:高階設計模式(優雅的類的拆分);

分層領域模型約定

聊完分層結構接下來我們說一下分層領域資料模型的約定,注意這裡的分層領域並不是指“DDD(領域驅動設計)模式”,而是對以上分層結構中各層之間互動資料物件的定義約定。在上述分層結構圖中已經標識了DTO、BO、PO的使用範圍(本規範只約定三種領域物件,事實上已經足夠,並不需要搞的太複雜)。具體如下:

寫了10年JAVA程式碼,為何還是給人一種亂糟糟的感覺?

在Controller層接收網路請求資料後,由於Controller層並不需要處理額外的邏輯,所以大部分情況下直接將DTO物件傳送給Service層;而Service層如果邏輯不復雜只是需要根據DTO的資料進行資料庫操作,那麼此時根據需要將DTO轉換為PO進行操作,完成後由於大部分場景下Service的輸出引數與輸入DTO物件都存在差異,因此為了區分我們將Service層的輸出資料物件統一定義為BO。

而Service層拆分時對於Manager層方法的輸入/輸出物件則統一為BO,包括Manager層操作第三方資料介面的資料物件轉換也統一為BO。以上劃分並沒有什麼特別的強制約定,而過分人為的去揣摩其含義本質上也沒什麼意義,只是大家共同遵守一個約定,這樣程式碼風格看起來會更加統一一點。

三、如何保持程式碼的簡潔性

作為一名對程式碼有追求的程式設計師,能少些一行程式碼就絕對不要囉嗦,而Java豐富的開源生態體系也給了我們這種懶惰很多便利,所以在程式設計的過程中其實是有很多工具可以幫助節省程式碼的。這裡給大家分別介紹三種方式:

MapStruct

在前面介紹的分層結構中,無論是DTO到BO,還是BO到PO亦或BO到BO,都會有很多的資料物件轉換的邏輯,傳統的方法是需要通過一堆Setter方法來完成的,而高階一點的lombok包提供的**@Builder**註解也是需要你寫一堆".build()"來完成資料的轉換,這樣的程式碼寫到Service層中顯然很浪費很多程式碼行,而MapStruct是一種更優雅的完成這件事的工具,使用方法如下:

專案pom.xml中引入依賴:

<!--MapStruct Java實體對映工具依賴-->
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-jdk8</artifactId>
    <version>1.3.1.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.3.1.Final</version>
</dependency>
複製程式碼

也需要在pom.xml引入一下Maven外掛:

<!--提供給MapStruct使用 -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
</plugin>
複製程式碼

之後編寫資料物件對映轉換介面:

package com.mafengwo.sales.sp.coupon.convert;

import com.mafengwo.sales.sp.coupon.client.bo.SalesCouponChannelBO;
import com.mafengwo.sales.sp.coupon.client.dto.SalesCouponChannelsDTO;
import com.mafengwo.sales.sp.coupon.dao.model.SalesCouponChannelsPO;
import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @author qiaojiang
 */
@Mapper
public interface SalesCouponChannelsConvert {

    SalesCouponChannelsConvert INSTANCE = Mappers.getMapper(SalesCouponChannelsConvert.class);

    @Mappings({
            @Mapping(target = "flag", expression = "java(java.lang.Integer.valueOf(\"0\"))"),
            @Mapping(target = "ctime", expression = "java(com.mafengwo.sales.sp.coupon.util.DateUtils.getCurrentTimestamp())"),
            @Mapping(target = "mtime", expression = "java(com.mafengwo.sales.sp.coupon.util.DateUtils.getCurrentTimestamp())")
    })
    SalesCouponChannelsPO convertSalesCouponChannelsPO(SalesCouponChannelsDTO salesCouponChannelsDTO);

    @Mappings({})
    List<SalesCouponChannelBO> convertCouponChannelBOList(List<SalesCouponChannelsPO> salesCouponChannelsPO);
}
複製程式碼

以上方法的入參為源資料物件,而返回物件則為目標資料物件,如果兩個物件的欄位名稱完成一致,那麼其實是不需要進行任何單獨對映的,直接 @Mappings({})即可;而如果對映物件之間欄位名稱有差異則可以通過@Mappings({@Mapping(target = "ctime", source = "createTime")})進行指定對映。而在業務層方法具體操作時使用方法如下:

//實體資料轉換
SalesCouponChannelsPO salesCouponChannelsPO = SalesCouponChannelsConvert.INSTANCE
        .convertSalesCouponChannelsPO(salesCouponChannelsDTO);
複製程式碼

這樣物件資料之間的拷貝將變得非常容易,從某種層面上看無論程式碼層次結構多麼繞,至少資料物件之間的拷貝將不再是一件麻煩的事!

lambada表示式

在Java8種提供了lambada表示式,在Java8中如果操作List相關資料結構,如果能夠使用lambada表示式也可以省一些程式碼,例如:

private List<Integer> getIdListFromEsSearchResult(SearchResult searchResult) {
    SearchHit[] searchHits = searchResult.getHits().getHits();
    List<Integer> idList = Arrays.asList(searchHits).stream().map(SearchHit::getSourceAsMap)
            .map(o -> Integer.parseInt(String.valueOf(o.get("id"))))
            .collect(Collectors.toList());
    return idList;
}
複製程式碼

有關lambada表示式更多的用法,大家有時間可以多看看相關語法知識,這裡就不再贅述!

tk.mybatis

在使用Mybatis框架作為資料庫開發框架時,相比較於Hibernate或其他JPA框架,Mybatis具有較強的對原生SQL的支援能力,因而會顯得比較靈活。但在大部分網際網路系統中,對資料庫的操作很多時候都是單表的操作,在這種情況下使用Mybatis也需要在Mapper程式碼和對映.xml檔案中編寫大量的SQL,而這些單表SQL本質上大同小異,完全可以通用化。

因此在Mybatis領域為了減少開發量很多專案會使用mybatis-generator外掛生成一份完整的對映程式碼,但是這樣的方式也會增加大量的無用程式碼,看起來並不是那麼的簡潔。而tk.mybatis則是考慮到了這個問題,可以兼顧對單表操作的便捷性(不需要再寫額外的程式碼)、多表聯合查詢的靈活性以及程式碼的簡潔性。具體用法如下:

專案pom.xml檔案引入相關依賴:

<!--Mybatis通用Mapper整合-->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.1.3</version>
    <exclusions>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper</artifactId>
    <version>4.1.3</version>
</dependency>
複製程式碼

主類@MapperScan註解換成tk.mybatis的:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchRestHealthIndicatorAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
//不要使用Mybatis原生註解,用tk.mybatis的
import tk.mybatis.spring.annotation.MapperScan;

import java.util.Date;

@SpringBootApplication(exclude = {ElasticSearchRestHealthIndicatorAutoConfiguration.class})
@ServletComponentScan
@EnableDiscoveryClient
@EnableWebMvc
@MonitorEnableAutoConfiguration
@MapperScan("com.mafengwo.sales.sp.coupon.dao.mapper")
@EnableTransactionManagement
public class SpCouponApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpCouponApplication.class, args);
    }
}
複製程式碼

編寫對映介面,單表操作將不再需要額外定義操作方法及對映SQL程式碼,而是可以直接用tk.mybatis提供的通用方法,程式碼如下:

import com.mafengwo.sales.sp.coupon.dao.model.CouponNameScopeRelationPO;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;

@Repository
public interface CouponNameScopeRelationMapper extends Mapper<CouponNameScopeRelationPO> {

}
複製程式碼

而在Mybatis SQL對映檔案*.xml中單表也只需要定義簡單的欄位對映即可,而不在需要定義通篇的SQL程式碼了,如下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.mafengwo.sales.sp.coupon.dao.mapper.SalesCouponChannelsMapper">
    <resultMap id="BaseResultMap" type="com.mafengwo.sales.sp.coupon.dao.model.SalesCouponChannelsPO">
        <id column="ID" property="id" jdbcType="INTEGER"/>
        <result column="NAME" property="name" jdbcType="VARCHAR"/>
        <result column="DESC" property="desc" jdbcType="VARCHAR"/>
        <result column="ADMIN_UID" property="adminUid" jdbcType="INTEGER"/>
        <result column="FLAG" property="flag" jdbcType="INTEGER"/>
        <result column="CTIME" property="ctime" jdbcType="TIMESTAMP"/>
        <result column="MTIME" property="mtime" jdbcType="TIMESTAMP"/>
        <result column="SCENEID" property="sceneId" jdbcType="INTEGER"/>
    </resultMap>
</mapper>
複製程式碼

除以上工具外,在實際的開發過程中還有很多開源或通過自定義元件的方式能夠讓程式碼寫的更簡潔,大家可以保持探索!

四、Java程式設計原則與設計模式

構建複雜的軟體系統只有遵循一定的設計原則併合適地運用相應地設計模式,這樣的程式碼才不至於在複雜的邏輯中迷失方向。關於設計原則及設計模式的話題是一個需要時間打磨和反覆歷練的修行,因此這裡只是為大家簡單陳列,在Java程式設計時應該遵循的一些原則以及可用的設計原則,做到心中有劍!

設計原則

單一職責(一個蘿蔔一個坑)、里氏替換(繼承複用)、依賴倒置(面向介面程式設計)、介面隔離(高內聚、低耦合)、迪米特法則(降低類與類之間的耦合)、開閉原則(對擴充套件開發、對修改關閉)。

設計模式

在Java領域,大概有23種設計模式,它們分別是:

  • 建立型模式:單例模式、抽象工廠模式、建造者模式、工廠模式、原型模式
  • 結構型模式:介面卡模式、橋接模式、裝飾模式、組合模式、外觀模式、享元模式、代理模式
  • 行為型模式:模板方法模式、命令模式、迭代器模式、觀察者模式、中介者模式、備忘錄模式、直譯器模式、狀態模式、策略模式

以上這些模式或多或少在我們日常的程式設計中都會見到或者聽過,但在平時能夠用到的卻並不多,很多原因在於目前Java領域的開發框架如Spring已經給我們做了很多的限定,而在大部分網際網路系統中,程式設計模式又很固定。在多數情況下,工廠模式的運用就能搞定大多數業務程式設計場景,因此很多模式只有在很多中介軟體系統等基礎軟體中被使用得比較多。通過羅列上述設計模式,並不是要大家為了設計而生硬的使用設計模式,而是要努力向著“心中有丘壑,眉目作山河”目標境界前進!只有這樣才能不至於日復一日的碼磚生涯中,迷失自我,失去方向!

後記

隨著時光的流逝,越來越多的程式設計師步入中年,寫了10多年程式碼的人也越來越多,而行業的發展卻在走下坡路,種種因素讓越來越多的人感到焦慮!個人覺得作為一名程式設計師,我們的核心能力還在於程式碼,因此在日復一日的碼磚生涯中不斷修煉自己的程式碼能力才是關鍵!否則可能就會出現被年輕人鄙視了!

相關文章