您好,登錄后才能下訂單哦!
接觸過不少號稱寫了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框架,總體上與阿里巴巴開發手冊應用分層方式一致,分層結構示意圖如下:
在基于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層拆分層次定義為以下三個等級:
聊完分層結構接下來我們說一下分層領域數據模型的約定,注意這里的分層領域并不是指“DDD(領域驅動設計)模式”,而是對以上分層結構中各層之間交互數據對象的定義約定。在上述分層結構圖中已經標識了DTO、BO、PO的使用范圍(本規范只約定三種領域對象,事實上已經足夠,并不需要搞的太復雜)。具體如下:
在Controller層接收網絡請求數據后,由于Controller層并不需要處理額外的邏輯,所以大部分情況下直接將DTO對象傳送給Service層;而Service層如果邏輯不復雜只是需要根據DTO的數據進行數據庫操作,那么此時根據需要將DTO轉換為PO進行操作,完成后由于大部分場景下Service的輸出參數與輸入DTO對象都存在差異,因此為了區分我們將Service層的輸出數據對象統一定義為BO。
而Service層拆分時對于Manager層方法的輸入/輸出對象則統一為BO,包括Manager層操作第三方數據接口的數據對象轉換也統一為BO。以上劃分并沒有什么特別的強制約定,而過分人為的去揣摩其含義本質上也沒什么意義,只是大家共同遵守一個約定,這樣代碼風格看起來會更加統一一點。
作為一名對代碼有追求的程序員,能少些一行代碼就絕對不要啰嗦,而Java豐富的開源生態體系也給了我們這種懶惰很多便利,所以在編程的過程中其實是有很多工具可以幫助節省代碼的。這里給大家分別介紹三種方式:
在前面介紹的分層結構中,無論是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);
這樣對象數據之間的拷貝將變得非常容易,從某種層面上看無論代碼層次結構多么繞,至少數據對象之間的拷貝將不再是一件麻煩的事!
在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表達式更多的用法,大家有時間可以多看看相關語法知識,這里就不再贅述!
在使用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領域,大概有23種設計模式,它們分別是:
以上這些模式或多或少在我們日常的編程中都會見到或者聽過,但在平時能夠用到的卻并不多,很多原因在于目前Java領域的開發框架如Spring已經給我們做了很多的限定,而在大部分互聯網系統中,編程模式又很固定。在多數情況下,工廠模式的運用就能搞定大多數業務編程場景,因此很多模式只有在很多中間件系統等基礎軟件中被使用得比較多。通過羅列上述設計模式,并不是要大家為了設計而生硬的使用設計模式,而是要努力向著“心中有丘壑,眉目作山河”目標境界前進!只有這樣才能不至于日復一日的碼磚生涯中,迷失自我,失去方向!
隨著時光的流逝,越來越多的程序員步入中年,寫了10多年代碼的人也越來越多,而行業的發展卻在走下坡路,種種因素讓越來越多的人感到焦慮!個人覺得作為一名程序員,我們的核心能力還在于代碼,因此在日復一日的碼磚生涯中不斷修煉自己的代碼能力才是關鍵!否則可能就會出現被年輕人鄙視了!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。