當前位置: 妍妍網 > 碼農

你見過哪些目瞪口呆的 Java 程式碼技巧?

2024-04-19碼農

自從畢業後,今年已經是我工作的第 8 個年頭了,我甚至都快忘記了到底是那年畢業的。從出來本人一直在做 Java 相關的工作,現在終於有時間坐下來,寫一篇關於 Java 寫法的一篇文章,來探討一下如果你真的是一個 Java 程式設計師,那你真的會寫 Java 嗎?

筆者是一個務實的程式設計師,故本文絕非扯淡文章,文中內容都是幹貨,望讀者看後,能有所收獲。

本文不是一個吹噓的文章,不會講很多高深的架構,相反,會講解很多基礎的問題和寫法問題,如果讀者自認為基礎問題和寫法問題都是不是問題,那請忽略這篇文章,節省出時間去做一些有意義的事情。

1 開發工具

不知道有多少」老」程式設計師還在使用 Eclipse,這些程式設計師們要不就是因循守舊,要不就是根本就不知道其他好的開發工具的存在,Eclipse 吃記憶體卡頓的現象以及各種偶然莫名異常的出現,都告知我們是時候尋找新的開發工具了。

更換 IDE

根本就不想多解釋要換什麽樣的 IDE,如果你想成為一個優秀的 Java 程式設計師,請更換 IntelliJ IDEA。使用 IDEA 的好處,請搜尋谷歌。

別告訴我快捷鍵不好用

更換 IDE 不在我本文的重點內容中,所以不想用太多的篇幅去寫為什麽更換IDE。在這裏,我只能告訴你,更換 IDE 只為了更好、更快的寫好 Java 程式碼。原因略。

別告訴我快捷鍵不好用,請嘗試新事物。

bean

bean 使我們使用最多的模型之一,我將以大篇幅去講解 bean,希望讀者好好體會。

domain 包名

根據很多 Java 程式設計師的」經驗」來看,一個資料庫表則對應著一個 domain 物件,所以很多程式設計師在寫程式碼時,包名則使用:com.xxx.domain ,這樣寫好像已經成為了行業的一種約束,資料庫對映物件就應該是 domain。但是你錯了,domain 是一個領域物件,往往我們再做傳統 Java 軟體 Web 開發中,這些 domain 都是貧血模型,是沒有行為的,或是沒有足夠的領域模型的行為的,所以,以這個理論來講,這些 domain 都應該是一個普通的 entity 物件,並非領域物件,所以請把包名改為:com.xxx.entity。

如果你還不理解我說的話,請看一下 Vaughn Vernon 出的一本叫做【IMPLEMENTING DOMAIN-DRIVEN DESIGN】(實作領域驅動設計)這本書,書中講解了貧血模型與領域模型的區別,相信你會受益匪淺。

DTO

數據傳輸我們應該使用 DTO 物件作為傳輸物件,這是我們所約定的,因為很長時間我一直都在做移動端 API 設計的工作,有很多人告訴我,他們認為只有給手機端傳輸數據的時候(input or output),這些物件成為 DTO 物件。請註意!這種理解是錯誤的,只要是用於網路傳輸的物件,我們都認為他們可以當做是 DTO 物件,比如電商平台中,使用者進行下單,下單後的數據,訂單會發到 OMS 或者 ERP 系統,這些對接的返回值以及入參也叫 DTO 物件。

我們約定某物件如果是 DTO 物件,就將名稱改為 XXDTO,比如訂單下發OMS:OMSOrderInputDTO。

DTO 轉化

正如我們所知,DTO 為系統與外界互動的模型物件,那麽肯定會有一個步驟是將 DTO 物件轉化為 BO 物件或者是普通的 entity 物件,讓 service 層去處理。

場景

比如添加會員操作,由於用於演示,我只考慮使用者的一些簡單數據,當後台管理員點選添加使用者時,只需要傳過來使用者的姓名和年齡就可以了,後端接受到數據後,將添加建立時間和更新時間和預設密碼三個欄位,然後保存資料庫。

@RequestMapping("/v1/api/user")
@RestController
public classUserApi{
@Autowired
private UserService userService;
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new User();
user.setUsername(userInputDTO.getUsername());
user.setAge(userInputDTO.getAge());
return userService.addUser(user);
}
}

我們只關註一下上述程式碼中的轉化程式碼,其他內容請忽略:

User user = new User();
user.setUsername(userInputDTO.getUsername());
user.setAge(userInputDTO.getAge());

請使用工具

上邊的程式碼,從邏輯上講,是沒有問題的,只是這種寫法讓我很厭煩,例子中只有兩個欄位,如果有 20 個欄位,我們要如何做呢? 一個一個進行 set 數據嗎?當然,如果你這麽做了,肯定不會有什麽問題,但是,這肯定不是一個最優的做法。

網上有很多工具,支持淺拷貝或深拷貝的 Utils。舉個例子,我們可以使用 org.springframework.beans.BeanUtils#copyProperties 對程式碼進行重構和最佳化:

@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return userService.addUser(user);
}

BeanUtils.copyProperties 是一個淺拷貝方法,復制內容時,我們只需要把 DTO 物件和要轉化的物件兩個的內容值設定為一樣的名稱,並且保證一樣的型別就可以了。如果你在做 DTO 轉化的時候一直使用 set 進行內容賦值,那麽請嘗試這種方式簡化程式碼,讓程式碼更加清晰!

轉化的語意

上邊的轉化過程,讀者看後肯定覺得優雅很多,但是我們再寫 Java 程式碼時,更多的需要考慮語意的操作,再看上邊的程式碼:

User user = new User();
BeanUtils.copyProperties(userInputDTO,user);

雖然這段程式碼很好的簡化和最佳化了程式碼,但是他的語意是有問題的,我們需要提現一個轉化過程才好,所以程式碼改成如下:

@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = convertFor(userInputDTO);
return userService.addUser(user);
 }
private User convertFor(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
 }

這是一個更好的語意寫法,雖然他麻煩了些,但是可讀性大大增加了,在寫程式碼時,我們應該盡量把語意層次差不多的放到一個方法中,比如:

User user = convertFor(userInputDTO);
return userService.addUser(user);

這兩段程式碼都沒有暴露實作,都是在講如何在同一個方法中,做一組相同層次的語意操作,而不是暴露具體的實作。

如上所述,是一種重構方式,讀者可以參考 Martin Fowler 的【Refactoring Imporving the Design of Existing Code】(重構 改善既有程式碼的設計) 這本書中的 Extract Method 重構方式。

抽象介面定義

當實際工作中,完成了幾個 API 的 DTO 轉化時,我們會發現,這樣的操作有很多很多,那麽應該定義好一個介面,讓所有這樣的操作都有規則的進行。

如果介面被定義以後,那麽 convertFor 這個方法的語意將產生變化,它將是一個實作類。

看一下抽象後的介面:

publicinterfaceDTOConvert<S,T{
convert(S s);
}

雖然這個介面很簡單,但是這裏告訴我們一個事情,要去使用泛型,如果你是一個優秀的 Java 程式設計師,請為你想做的抽象介面,做好泛型吧。

我們再來看介面實作:

public classUserInputDTOConvertimplementsDTOConvert{
@Override
public User convert(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}

我們這樣重構後,我們發現現在的程式碼是如此的簡潔,並且那麽的規範:

@RequestMapping("/v1/api/user")
@RestController
public classUserApi{
@Autowired
private UserService userService;
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new UserInputDTOConvert().convert(userInputDTO);
return userService.addUser(user);
}
}

review code

如果你是一個優秀的 Java 程式設計師,我相信你應該和我一樣,已經數次重復 review 過自己的程式碼很多次了。

我們再看這個保存使用者的例子,你將發現,API 中返回值是有些問題的,問題就在於不應該直接返回 User 實體,因為如果這樣的話,就暴露了太多實體相關的資訊,這樣的返回值是不安全的,所以我們更應該返回一個 DTO 物件,我們可稱它為 UserOutputDTO:

@PostMapping
public UserOutputDTO addUser(UserInputDTO userInputDTO){
User user = new UserInputDTOConvert().convert(userInputDTO);
User saveUserResult = userService.addUser(user);
UserOutputDTO result = new UserOutDTOConvert().convertToUser(saveUserResult);
return result;
}

這樣你的 API 才更健全。

不知道在看完這段程式碼之後,讀者有是否發現還有其他問題的存在,作為一個優秀的 Java 程式設計師,請看一下這段我們剛剛抽象完的程式碼:

User user = new UserInputDTOConvert().convert(userInputDTO);

你會發現,new 這樣一個 DTO 轉化物件是沒有必要的,而且每一個轉化物件都是由在遇到 DTO 轉化的時候才會出現,那我們應該考慮一下,是否可以將這個類和 DTO 進行聚合呢,看一下我的聚合結果:

public classUserInputDTO{
private String username;
privateint age;
public String getUsername(){
return username;
}
publicvoidsetUsername(String username){
this.username = username;
}
publicintgetAge(){
return age;
}
publicvoidsetAge(int age){
this.age = age;
}

public User convertToUser(){
UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();
User convert = userInputDTOConvert.convert(this);
return convert;
}
privatestatic classUserInputDTOConvertimplementsDTOConvert<UserInputDTO,User{
@Override
public User convert(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
}





然後 API 中的轉化則由:

User user = new UserInputDTOConvert().convert(userInputDTO);
User saveUserResult = userService.addUser(user);

變成了:

User user = userInputDTO.convertToUser();
User saveUserResult = userService.addUser(user);

我們再 DTO 物件中添加了轉化的行為,我相信這樣的操作可以讓程式碼的可讀性變得更強,並且是符合語意的。

再查工具類

再來看 DTO 內部轉化的程式碼,它實作了我們自己定義的 DTOConvert 介面,但是這樣真的就沒有問題,不需要再思考了嗎?

我覺得並不是,對於 Convert 這種轉化語意來講,很多工具類中都有這樣的定義,這中 Convert 並不是業務級別上的介面定義,它只是用於普通 bean 之間轉化內容值的普通意義上的介面定義,所以我們應該更多的去讀其他含有 Convert 轉化語意的程式碼。

我仔細閱讀了一下 GUAVA 的源碼,發現了 com.google.common.base.Convert 這樣的定義:

publicabstract classConverter<ABimplementsFunction<AB{
protectedabstract B doForward(A a);
protectedabstract A doBackward(B b);
//其他略
}

從源碼可以了解到,GUAVA 中的 Convert 可以完成正向轉化和逆向轉化,繼續修改我們 DTO 中轉化的這段程式碼:

privatestatic classUserInputDTOConvertimplementsDTOConvert<UserInputDTO,User{
@Override
public User convert(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}

修改後:

privatestatic classUserInputDTOConvertextendsConverter<UserInputDTOUser{
@Override
protected User doForward(UserInputDTO userInputDTO){
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
@Override
protected UserInputDTO doBackward(User user){
UserInputDTO userInputDTO = new UserInputDTO();
BeanUtils.copyProperties(user,userInputDTO);
return userInputDTO;
}
 }

看了這部份程式碼以後,你可能會問,那逆向轉化會有什麽用呢?其實我們有很多小的業務需求中,入參和出參是一樣的,那麽我們變可以輕松的進行轉化,我將上邊所提到的 UserInputDTO 和 UserOutputDTO 都轉成 UserDTO 展示給大家。

DTO:

public classUserDTO{
private String username;
privateint age;
public String getUsername(){
return username;
}
publicvoidsetUsername(String username){
this.username = username;
}
publicintgetAge(){
return age;
}
publicvoidsetAge(int age){
this.age = age;
}

public User convertToUser(){
UserDTOConvert userDTOConvert = new UserDTOConvert();
User convert = userDTOConvert.convert(this);
return convert;
}
public UserDTO convertFor(User user){
UserDTOConvert userDTOConvert = new UserDTOConvert();
UserDTO convert = userDTOConvert.reverse().convert(user);
return convert;
}
privatestatic classUserDTOConvertextendsConverter<UserDTOUser{
@Override
protected User doForward(UserDTO userDTO){
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}
@Override
protected UserDTO doBackward(User user){
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user,userDTO);
return userDTO;
}
}
}







API:

@PostMapping
public UserDTO addUser(UserDTO userDTO){
User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
 }

當然,上述只是表明了轉化方向的正向或逆向,很多業務需求的出參和入參的 DTO 物件是不同的,那麽你需要更明顯的告訴程式:逆向是無法呼叫的:

privatestatic classUserDTOConvertextendsConverter<UserDTOUser{
@Override
protected User doForward(UserDTO userDTO){
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}
@Override
protected UserDTO doBackward(User user){
thrownew AssertionError("不支持逆向轉化方法!");
}
 }

看一下 doBackward 方法,直接丟擲了一個斷言異常,而不是業務異常,這段程式碼告訴程式碼的呼叫者,這個方法不是準你呼叫的,如果你呼叫,我就」斷言」你呼叫錯誤了。

關於例外處理的更詳細介紹,可以參考這篇文章:如何優雅的設計 Java 異常(http://dw-z.ink/1Tak-) ,應該可以幫你更好的理解異常。

bean 的驗證

如果你認為我上邊寫的那個添加使用者 API 寫的已經非常完美了,那只能說明你還不是一個優秀的程式設計師。我們應該保證任何數據的入參到方法體內都是合法的。

為什麽要驗證

很多人會告訴我,如果這些 API 是提供給前端進行呼叫的,前端都會進行驗證啊,你為什還要驗證?

其實答案是這樣的,我從不相信任何呼叫我 API 或者方法的人,比如前端驗證失敗了,或者某些人透過一些特殊的渠道(比如 Charles 進行抓包),直接將數據傳入到我的 API,那我仍然進行正常的業務邏輯處理,那麽就有可能產生臟數據!

「對於臟數據的產生一定是致命」,這句話希望大家牢記在心,再小的臟數據也有可能讓你找幾個通宵!

jsr 303驗證

hibernate 提供的 jsr 303 實作,我覺得目前仍然是很優秀的,具體如何使用,我不想講,因為谷歌上你可以搜尋出很多答案!

再以上班的 API 例項進行說明,我們現在對 DTO 數據進行檢查:

public classUserDTO{
@NotNull
private String username;
@NotNull
privateint age;
//其他程式碼略
}

API 驗證:

@PostMapping
public UserDTO addUser(@Valid UserDTO userDTO){
User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}

我們需要將驗證結果傳給前端,這種異常應該轉化為一個 api 異常(帶有錯誤碼的異常)。

@PostMapping
public UserDTO addUser(@Valid UserDTO userDTO, BindingResult bindingResult){
checkDTOParams(bindingResult);
User user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}
privatevoidcheckDTOParams(BindingResult bindingResult){
if(bindingResult.hasErrors()){
//throw new 帶驗證碼的驗證錯誤異常
}
}

BindingResult 是 Spring MVC 驗證 DTO 後的一個結果集,可以參考spring 官方文件(spring.io/)。

檢查參數後,可以丟擲一個「帶驗證碼的驗證錯誤異常」

具體可以參考這篇:很優秀的文章

http://lrwinx.github.io/2016/04/28/如何优雅的设计java异常/

擁抱 lombok

上邊的 DTO 程式碼,已經讓我看的很累了,我相信讀者也是一樣,看到那麽多的 Getter 和 Setter 方法,太煩躁了,那時候有什麽方法可以簡化這些呢。

請擁抱 lombok,它會幫助我們解決一些讓我們很煩躁的問題

去掉 Setter 和 Getter

其實這個標題,我不太想說,因為網上太多,但是因為很多人告訴我,他們根本就不知道 lombok 的存在,所以為了讓讀者更好的學習,我願意寫這樣一個例子:

@Setter
@Getter
public classUserDTO{
@NotNull
private String username;
@NotNull
privateint age;
public User convertToUser(){
UserDTOConvert userDTOConvert = new UserDTOConvert();
User convert = userDTOConvert.convert(this);
return convert;
}
public UserDTO convertFor(User user){
UserDTOConvert userDTOConvert = new UserDTOConvert();
UserDTO convert = userDTOConvert.reverse().convert(user);
return convert;
}
privatestatic classUserDTOConvertextendsConverter<UserDTOUser{
@Override
protected User doForward(UserDTO userDTO){
User user = new User();
BeanUtils.copyProperties(userDTO,user);
return user;
}
@Override
protected UserDTO doBackward(User user){
thrownew AssertionError("不支持逆向轉化方法!");
}
}
}



看到了吧,煩人的 Getter 和 Setter 方法已經去掉了。

但是上邊的例子根本不足以體現 lombok 的強大。我希望寫一些網上很難查到,或者很少人進行說明的 lombok 的使用以及在使用時程式語意上的說明。

比如:@Data,@AllArgsConstructor,@NoArgsConstructor..這些我就不進行一一說明了,請大家自行查詢資料。

bean 中的鏈式風格

什麽是鏈式風格?我來舉個例子,看下面這個 Student 的 bean:

public classStudent{
private String name;
privateint age;
public String getName(){
return name;
}
public Student setName(String name){
this.name = name;
returnthis;
}
publicintgetAge(){
return age;
}
public Student setAge(int age){
returnthis;
}
}


仔細看一下 set 方法,這樣的設定便是 chain 的 style,呼叫的時候,可以這樣使用:

Student student = new Student()
.setAge(24)
.setName("zs");

相信合理使用這樣的鏈式程式碼,會更多的程式帶來很好的可讀性,那看一下如果使用 lombok 進行改善呢,請使用 @Accessors(chain = true),看如下程式碼:

@Accessors(chain = true)
@Setter
@Getter
public classStudent{
private String name;
privateint age;
}

這樣就完成了一個對於 bean 來講很友好的鏈式操作。

靜態構造方法

靜態構造方法的語意和簡化程度真的高於直接去 new 一個物件。比如 new 一個 List 物件,過去的使用是這樣的:

List<String> list = new ArrayList<>();

看一下 guava 中的建立方式:

List<String> list = Lists.newArrayList();

Lists 命名是一種約定(俗話說:約定優於配置),它是指 Lists 是 List 這個類的一個工具類,那麽使用 List 的工具類去產生 List,這樣的語意是不是要比直接 new 一個子類別來的更直接一些呢,答案是肯定的,再比如如果有一個工具類叫做 Maps,那你是否想到了建立 Map 的方法呢:

HashMap<String, String> objectObjectHashMap = Maps.newHashMap();

好了,如果你理解了我說的語意,那麽,你已經向成為 Java 程式設計師更近了一步了。

再回過頭來看剛剛的 Student,很多時候,我們去寫 Student 這個 bean 的時候,他會有一些必輸欄位,比如 Student 中的 name 欄位,一般處理的方式是將 name 欄位包裝成一個構造方法,只有傳入 name 這樣的構造方法,才能建立一個 Student 物件。

接上上邊的靜態構造方法和必傳參數的構造方法,使用 lombok 將更改成如下寫法(@RequiredArgsConstructor 和 @NonNull):

@Accessors(chain = true)
@Setter
@Getter
@RequiredArgsConstructor(staticName = "ofName")
public classStudent{
@NonNullprivate String name;
privateint age;
}

測試程式碼:

Student student = Student.ofName("zs");

這樣構建出的 bean 語意是否要比直接 new 一個含參的構造方法(包含 name 的構造方法)要好很多。

當然,看過很多源碼以後,我想相信將靜態構造方法 ofName 換成 of 會先的更加簡潔:

@Accessors(chain = true)
@Setter
@Getter
@RequiredArgsConstructor(staticName = "of")
public classStudent{
@NonNullprivate String name;
privateint age;
}

測試程式碼:

Student student = Student.of("zs");

當然他仍然是支持鏈式呼叫的:

Student student = Student.of("zs").setAge(24);

這樣來寫程式碼,真的很簡潔,並且可讀性很強。

使用 builder

Builder 模式我不想再多解釋了,讀者可以看一下【Head First】(設計模式) 的建造者模式。

今天其實要說的是一種變種的 builder 模式,那就是構建 bean 的 builder 模式,其實主要的思想是帶著大家一起看一下 lombok 給我們帶來了什麽。

看一下 Student 這個類的原始 builder 狀態:

public classStudent{
private String name;
privateint age;
public String getName(){
return name;
}
publicvoidsetName(String name){
this.name = name;
}
publicintgetAge(){
return age;
}
publicvoidsetAge(int age){
this.age = age;
}
publicstatic Builder builder(){
returnnew Builder();
}
publicstatic classBuilder{
private String name;
privateint age;
public Builder name(String name){
this.name = name;
returnthis;
}
public Builder age(int age){
this.age = age;
returnthis;
}
public Student build(){
Student student = new Student();
student.setAge(age);
student.setName(name);
return student;
}
}
}






呼叫方式:

Student student = Student.builder().name("zs").age(24).build();

這樣的 builder 程式碼,讓我是在惡心難受,於是我打算用 lombok 重構這段程式碼:

@Builder
public classStudent{
private String name;
privateint age;
}

呼叫方式:

Student student = Student.builder().name("zs").age(24).build();

代理模式

正如我們所知的,在程式中呼叫 rest 介面是一個常見的行為動作,如果你和我一樣使用過 spring 的 RestTemplate,我相信你會我和一樣,對他丟擲的非 http 狀態碼異常深惡痛絕。

所以我們考慮將 RestTemplate 最為底層包裝器進行包裝器模式的設計:

publicabstract classFilterRestTemplateimplementsRestOperations{
protectedvolatile RestTemplate restTemplate;
protectedFilterRestTemplate(RestTemplate restTemplate){
this.restTemplate = restTemplate;
}
//實作RestOperations所有的介面
}

然後再由擴充套件類對 FilterRestTemplate 進行包裝擴充套件:

public classExtractRestTemplateextendsFilterRestTemplate{
private RestTemplate restTemplate;
publicExtractRestTemplate(RestTemplate restTemplate){
super(restTemplate);
this.restTemplate = restTemplate;
}
public <T> RestResponseDTO<T> postForEntityWithNoException(String url, Object request, class<T> responseType, Object... uriVariables)
throws RestClientException 
{
RestResponseDTO<T> restResponseDTO = new RestResponseDTO<T>();
ResponseEntity<T> tResponseEntity;
try {
tResponseEntity = restTemplate.postForEntity(url, request, responseType, uriVariables);
restResponseDTO.setData(tResponseEntity.getBody());
restResponseDTO.setMessage(tResponseEntity.getStatusCode().name());
restResponseDTO.setStatusCode(tResponseEntity.getStatusCodeValue());
}catch (Exception e){
restResponseDTO.setStatusCode(RestResponseDTO.UNKNOWN_ERROR);
restResponseDTO.setMessage(e.getMessage());
restResponseDTO.setData(null);
}
return restResponseDTO;
}
}

包裝器 ExtractRestTemplate 很完美的更改了異常丟擲的行為,讓程式更具有容錯性。在這裏我們不考慮 ExtractRestTemplate 完成的功能,讓我們把焦點放在 FilterRestTemplate 上,「實作 RestOperations 所有的介面」,這個操作絕對不是一時半會可以寫完的,當時在重構之前我幾乎寫了半個小時,如下:

publicabstract classFilterRestTemplateimplementsRestOperations{
protectedvolatile RestTemplate restTemplate;
protectedFilterRestTemplate(RestTemplate restTemplate){
this.restTemplate = restTemplate;
}
@Override
public <T> getForObject(String url, class<T> responseType, Object... uriVariables)throws RestClientException {
return restTemplate.getForObject(url,responseType,uriVariables);
}
@Override
public <T> getForObject(String url, class<T> responseType, Map<String, ?> uriVariables)throws RestClientException {
return restTemplate.getForObject(url,responseType,uriVariables);
}
@Override
public <T> getForObject(URI url, class<T> responseType)throws RestClientException {
return restTemplate.getForObject(url,responseType);
}
@Override
public <T> ResponseEntity<T> getForEntity(String url, class<T> responseType, Object... uriVariables)throws RestClientException {
return restTemplate.getForEntity(url,responseType,uriVariables);
}
//其他實作程式碼略。。。
}




我相信你看了以上程式碼,你會和我一樣覺得惡心反胃,後來我用 lombok 提供的代理註解最佳化了我的程式碼(@Delegate):

@AllArgsConstructor
publicabstract classFilterRestTemplateimplementsRestOperations{
@Delegate
protectedvolatile RestTemplate restTemplate;
}

這幾行程式碼完全替代上述那些冗長的程式碼。

是不是很簡潔,做一個擁抱 lombok 的程式設計師吧。

2 重構

需求案例

計畫需求

計畫開發階段,有一個關於下單發貨的需求:如果今天下午 3 點前進行下單,那麽發貨時間是明天,如果今天下午 3 點後進行下單,那麽發貨時間是後天,如果被確定的時間是周日,那麽在此時間上再加 1 天為發貨時間。

思考與重構

我相信這個需求看似很簡單,無論怎麽寫都可以完成。

很多人可能看到這個需求,就動手開始寫 Calendar 或 Date 進行計算,從而完成需求。

而我給的建議是,仔細考慮如何寫程式碼,然後再去寫,不是說所有的時間操作都用 Calendar 或 Date 去解決,一定要看場景。

對於時間的計算我們要考慮 joda-time 這種類似的成熟時間計算框架來寫程式碼,它會讓程式碼更加簡潔和易讀。

請讀者先考慮這個需求如何用 Java 程式碼完成,或先寫一個你覺得完成這個程式碼的思路,再來看我下邊的程式碼,這樣,你的收獲會更多一些:

final DateTime DISTRIBUTION_TIME_SPLIT_TIME = new DateTime().withTime(15,0,0,0);
private Date calculateDistributionTimeByOrderCreateTime(Date orderCreateTime){
DateTime orderCreateDateTime = new DateTime(orderCreateTime);
Date tomorrow = orderCreateDateTime.plusDays(1).toDate();
Date theDayAfterTomorrow = orderCreateDateTime.plusDays(2).toDate();
return orderCreateDateTime.isAfter(DISTRIBUTION_TIME_SPLIT_TIME) ? wrapDistributionTime(theDayAfterTomorrow) : wrapDistributionTime(tomorrow);
}
private Date wrapDistributionTime(Date distributionTime){
DateTime currentDistributionDateTime = new DateTime(distributionTime);
DateTime plusOneDay = currentDistributionDateTime.plusDays(1);
boolean isSunday = (DateTimeConstants.SUNDAY == currentDistributionDateTime.getDayOfWeek());
return isSunday ? plusOneDay.toDate() : currentDistributionDateTime.toDate() ;
}

讀這段程式碼的時候,你會發現,我將判斷和有可能出現的不同結果都當做一個變量,最終做一個三目運算子的方式進行返回,這樣的優雅和可讀性顯而易見,當然這樣的程式碼不是一蹴而就的,我最佳化了 3 遍產生的以上程式碼。讀者可根據自己的程式碼和我寫的程式碼進行對比。

提高方法

如果你做了 3 年+的程式設計師,我相信像如上這樣的需求,你很輕松就能完成,但是如果你想做一個會寫 Java 的程式設計師,就好好的思考和重構程式碼吧。

寫程式碼就如同寫字一樣,同樣的字,大家都會寫,但是寫出來是否好看就不一定了。如果想把程式寫好,就要不斷的思考和重構,敢於嘗試,敢於創新,不要因循守舊,一定要做一個優秀的 Java 程式設計師。

提高程式碼水平最好的方法就是有條理的重構!(註意:是有條理的重構)

設計模式

設計模式就是工具,而不是提現你是否是高水平程式設計師的一個指標。

我經常會看到某一個程式設計師興奮的大喊,哪個程式哪個點我用到了設計模式,寫的多麽多麽優秀,多麽多麽好。我仔細去翻閱的時候,卻發現有很多是過度設計的。

業務驅動技術 or 技術驅動業務

業務驅動技術 or 技術驅動業務 ? 其實這是一個一直在爭論的話題,但是很多人不這麽認為,我覺得就是大家不願意承認罷了。我來和大家大概分析一下作為一個 Java 程式設計師,我們應該如何判斷自己所處於的位置.

業務驅動技術: 如果你所在的計畫是一個收益很小或者甚至沒有收益的計畫,請不要搞其他創新的東西,不要驅動業務要如何如何做,而是要熟知業務現在的痛點是什麽?如何才能幫助業務盈利或者讓計畫更好,更順利的進行。

技術驅動業務: 如果你所在的計畫是一個很牛的計畫,比如淘寶這類的計畫,我可以在滿足業務需求的情況下,和業務溝通,使用什麽樣的技術能更好的幫助業務創造收益,比如說下單的時候要進佇列,可能幾分鐘之後訂單狀態才能處理完成,但是會讓使用者有更流暢的體驗,賺取更多的存取流量,那麽我相信業務願意被技術驅動,會同意訂單的延遲問題,這樣便是技術驅動業務。

我相信大部份人還都處於業務驅動技術的方向吧。

所以你既然不能驅動業務,那就請擁抱業務變化吧。

程式碼設計

一直在做 Java 後端的計畫,經常會有一些變動,我相信大家也都遇到過。

比如當我們寫一段程式碼的時候,我們考慮將需求對映成程式碼的狀態模式,突然有一天,狀態模式裏邊又添加了很多行為變化的東西,這時候你就撓頭了,你硬生生的將狀態模式中添加過多行為和變化。

慢慢的你會發現這些狀態模式,其實更像是一簇演算法,應該使用策略模式,這時你應該已經暈頭轉向了。

說了這麽多,我的意思是,只要你覺得合理,就請將狀態模式改為策略模式吧,所有的模式並不是憑空想象出來的,都是基於重構。

Java 編程中沒有銀彈,請擁抱業務變化,一直思考重構,你就有一個更好的程式碼設計!

你真的優秀嗎?

真不好意思,我取了一個這麽無聊的標題。

國外流行一種編程方式,叫做結對編程,我相信國內很多公司都沒有這麽做,我就不在講述結對編程帶來的好處了,其實就是一邊 code review,一邊互相提高的一個過程。既然做不到這個,那如何讓自己活在自己的世界中不斷提高呢?

「平時開發的時候,做出的程式碼總認為是正確的,而且寫法是完美的。」,我相信這是大部份人的心聲,還回到剛剛的問題,如何在自己的世界中不斷提高呢?

答案就是:

  1. 多看成熟框架的源碼

  2. 多回頭看自己的程式碼

  3. 勤於重構

你真的優秀嗎? 如果你每周都完成了學習源碼,回頭看自己程式碼,然後勤於重構,我認為你就真的很優秀了。

即使也許你只是剛剛入門,但是一直堅持,你就是一個真的會寫java程式碼的程式設計師了。

3 技能

UML

不想多討論 UML 相關的知識,但是我覺得你如果真的會寫 Java,請先學會表達自己,UML 就是你說話的語言,做一名優秀的 Java 程式設計師,請至少學會這兩種 UML 圖:

  1. 類圖

  2. 時序圖

clean code

我認為保持程式碼的簡潔和可讀性是程式碼的最基本保證,如果有一天為了程式的效率而降低了這兩點,我認為是可以諒解的,除此之外,沒有任何理由可以讓你任意揮霍你的程式碼。

  1. 讀者可以看一下 Robert C. Martin 出版的【Clean Code】(程式碼整潔之道) 這本書

  2. 可以參考美團文章聊聊 clean code(http://tech.meituan.com/clean-code.html);

  3. 也可以看一下阿裏的 Java 編碼規範(https://yq.aliyun.com/articles/69327)。

無論如何,請保持你的程式碼的整潔。

Linux 基礎命令

這點其實和會寫 Java 沒有關系,但是 Linux 很多時候確實承載執行 Java 的容器,請學好 Linux 的基礎命令。

  1. 參考鳥哥的【Linux私房菜】

4 總結

Java 是一個大體系,今天討論並未涉及框架和架構相關知識,只是討論如何寫好程式碼。

本文從寫 Java 程式的小方面一直寫到大方面,來闡述了如何才能寫好 Java 程式,並告訴讀者們如何才能提高自身的編碼水平。

我希望看到這篇文章的各位都能做一個優秀的 Java 程式設計師

來源:juejin.cn/post/6844903954308939784

>>

END

精品資料,超贊福利,免費領

微信掃碼/長按辨識 添加【技術交流群

群內每天分享精品學習資料

最近開發整理了一個用於速刷面試題的小程式;其中收錄了上千道常見面試題及答案(包含基礎並行JVMMySQLRedisSpringSpringMVCSpringBootSpringCloud訊息佇列等多個型別),歡迎您的使用。

👇👇

👇點選"閱讀原文",獲取更多資料(持續更新中