細思極恐 :你真的會寫 Java 嗎?

很是很是不錯的一篇文章,文中提到的不少實踐都值得咱們去學習,強烈推薦閱讀 3 遍以上。若是以爲不錯的不要忘記點個在看,先點在看再看文章也是能夠的。

原文地址:http://lrwinx.github.io/2017/...html

做者: Lrwin前端

導語

自 2013 年畢業後,今年已是我工做的第 4 個年頭了,總在作 java 相關的工做,終於有時間坐下來,寫一篇關於 java 寫法的一篇文章,來探討一下若是你真的是一個 java 程序員,那你真的會寫 java 嗎?
筆者是一個務實的程序員,故本文絕非扯淡文章,文中內容都是乾貨,望讀者看後,能有所收穫。

其實,本不想把標題寫的那麼恐怖,只是發現不少人幹了幾年 java 之後,都自認爲是一個不錯的 java 程序員了,能夠拿着上萬的工資都處宣揚本身了,寫這篇文章的目的並非嘲諷和我同樣作 java 的同行們,只是但願讀者看到此騙文章後,能夠和我同樣,心平氣和的爭取作一個優秀的程序員。java

講述方向

因爲一直從事移動互聯網相關工做,java 開發中常常和移動端打交道或者作一些後端的工做,因此本篇文章更可能涉及和移動端的交互或者與後端的交互方式,筆者但願以自身的一些學習經驗或者開發經驗,能夠帶動認真閱讀本篇文章的讀者們,讓你們對 java 有一個更好的態度去學習它,它不僅是一個賺錢的工具而已。linux

筆者身邊有不少與筆者年齡相仿或年齡更大的朋友或同事,常常有人問我:「你如今還在學習嗎?我以爲沒什麼好學的,這些東西都差很少」,我老是回答只要有時間,我就要看一會書,這個時候,你們都會露出一副不屑的眼神或笑容。其實,很是能理解身邊朋友或同事的見解,以目前狀態來說,大多都是工做至少 5 年的程序員了,對於公司大大小小的業務須要,以目前的知識儲備來說,均可以輕鬆應對,「沒有什麼好學的」其實這句話沒有多大的問題,可是,若是你對編程還有一點點興趣,只是不知道如何努力或改進,但願本篇文章能夠幫到你。git

技術點

本文不是一個吹噓的文章,不會講不少高深的架構,相反,會講解不少基礎的問題和寫法問題,若是讀者自認爲基礎問題和寫法問題都是否是問題,那請忽略這篇文章,節省出時間去作一些有意義的事情。程序員

開發工具

不知道有多少」老」程序員還在使用 Eclipse,這些程序員們要不就是因循守舊,要不就是根本就不知道其餘好的開發工具的存在,Eclipse 吃內存卡頓的現象以及各類偶然莫名異常的出現,都告知咱們是時候尋找新的開發工具了。github

更換 IDE

根本就不想多解釋要換什麼樣的 IDE,若是你想成爲一個優秀的 Java 程序員,請更換 IDEA。 使用 IDEA 的好處,請搜索谷歌。web

別告訴我快捷鍵很差用

更換 IDE 不在我本文的重點內容中,因此不下想用太多的篇幅去寫爲何更換 IDE,請谷歌。算法

在這裏,我只能告訴你,更換 IDE 只爲了更好、更快的寫好 java 代碼。緣由略。spring

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

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 class UserApi {

    @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 這個方法的語義將產生變化,他將是一個實現類。

看一下抽象後的接口:

public interface DTOConvert<S,T> {
    T convert(S s);
}

雖然這個接口很簡單,可是這裏告訴咱們一個事情,要去使用泛型,若是你是一個優秀的 java 程序員,請爲你想作的抽象接口,作好泛型吧。

咱們再來看接口實現:

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

咱們這樣重構後,咱們發現如今的代碼是如此的簡潔,而且那麼的規範:

@RequestMapping("/v1/api/user")
@RestController
public class UserApi {

    @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 class UserInputDTO {
    private String username;
    private int age;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }


    public User convertToUser(){
        UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();
        User convert = userInputDTOConvert.convert(this);
        return convert;
    }

    private static class UserInputDTOConvert implements DTOConvert<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 這樣的定義:

public abstract class Converter<A, B> implements Function<A, B> {
    protected abstract B doForward(A a);
    protected abstract A doBackward(B b);
    //其餘略
}

從源碼能夠了解到,GUAVA 中的 Convert 能夠完成正向轉化和逆向轉化,繼續修改咱們 DTO 中轉化的這段代碼:

private static class UserInputDTOConvert implements DTOConvert<UserInputDTO,User> {
        @Override
        public User convert(UserInputDTO userInputDTO) {
                User user = new User();
                BeanUtils.copyProperties(userInputDTO,user);
                return user;
        }
}

修改後:

private static class UserInputDTOConvert extends Converter<UserInputDTO, User> {
         @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 class UserDTO {
    private String username;
    private int age;

    public String getUsername() {
            return username;
    }

    public void setUsername(String username) {
            this.username = username;
    }

    public int getAge() {
            return age;
    }

    public void setAge(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;
    }

    private static class UserDTOConvert extends Converter<UserDTO, User> {
            @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 對象是不一樣的,那麼你須要更明顯的告訴程序:逆向是沒法調用的:

private static class UserDTOConvert extends Converter<UserDTO, User> {
         @Override
         protected User doForward(UserDTO userDTO) {
                 User user = new User();
                 BeanUtils.copyProperties(userDTO,user);
                 return user;
         }

         @Override
         protected UserDTO doBackward(User user) {
                 throw new AssertionError("不支持逆向轉化方法!");
         }
 }

看一下 doBackward 方法,直接拋出了一個斷言異常,而不是業務異常,這段代碼告訴代碼的調用者,這個方法不是準你調用的,若是你調用,我就」斷言」你調用錯誤了。

bean 的驗證

若是你認爲我上邊寫的那個添加用戶 api 寫的已經很是完美了,那隻能說明你還不是一個優秀的程序員。咱們應該保證任何數據的入參到方法體內都是合法的。

爲何要驗證

不少人會告訴我,若是這些 api 是提供給前端進行調用的,前端都會進行驗證啊,你爲什還要驗證?
其實答案是這樣的,我從不相信任何調用我 api 或者方法的人,好比前端驗證失敗了,或者某些人經過一些特殊的渠道(好比 Charles 進行抓包),直接將數據傳入到個人 api,那我仍然進行正常的業務邏輯處理,那麼就有可能產生髒數據!
「對於髒數據的產生必定是致命」,這句話但願你們牢記在心,再小的髒數據也有可能讓你找幾個通宵!

jsr 303 驗證

hibernate 提供的 jsr 303 實現,我以爲目前仍然是很優秀的,具體如何使用,我不想講,由於谷歌上你能夠搜索出不少答案!
再以上班的 api 實例進行說明,咱們如今對 DTO 數據進行檢查:

public class UserDTO {
    @NotNull
    private String username;
    @NotNull
    private int 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;
}
private void checkDTOParams(BindingResult bindingResult){
     if(bindingResult.hasErrors()){
             //throw new 帶驗證碼的驗證錯誤異常
     }
}

BindingResult 是 Spring MVC 驗證 DTO 後的一個結果集,能夠參考spring 官方文檔

擁抱 lombok

上邊的 DTO 代碼,已經讓我看的很累了,我相信讀者也是同樣,看到那麼多的 Getter 和 Setter 方法,太煩躁了,那時候有什麼方法能夠簡化這些呢。
請擁抱 lombok,它會幫助咱們解決一些讓咱們很煩躁的問題

去掉 Setter 和 Getter

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

@Setter
@Getter
public class UserDTO {
    @NotNull
    private String username;
    @NotNull
    private int 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;
    }

    private static class UserDTOConvert extends Converter<UserDTO, User> {
        @Override
        protected User doForward(UserDTO userDTO) {
            User user = new User();
            BeanUtils.copyProperties(userDTO,user);
            return user;
        }

        @Override
        protected UserDTO doBackward(User user) {
            throw new AssertionError("不支持逆向轉化方法!");
        }
    }

}

看到了吧,煩人的 Getter 和 Setter 方法已經去掉了。
可是上邊的例子根本不足以體現 lombok 的強大。我但願寫一些網上很難查到,或者不多人進行說明的 lombok 的使用以及在使用時程序語義上的說明。
好比:@Data,@AllArgsConstructor,@NoArgsConstructor..這些我就不進行一一說明了,請你們自行查詢資料.

bean 中的鏈式風格

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

public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public Student setName(String name) {
        this.name = name;
        return this;
    }

    public int getAge() {
        return age;
    }

    public Student setAge(int age) {
        return this;
    }
}

仔細看一下 set 方法,這樣的設置即是 chain 的 style,調用的時候,能夠這樣使用:

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

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

@Accessors(chain = true)
@Setter
@Getter
public class Student {
    private String name;
    private int 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 class Student {
    @NonNull private String name;
    private int age;
}

測試代碼:

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

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

固然,看過不少源碼之後,我想相信將靜態構造方法 ofName 換成 of 會先的更加簡潔:

@Accessors(chain = true)
@Setter
@Getter
@RequiredArgsConstructor(staticName = "of")
public class Student {
        @NonNull private String name;
        private int age;
}

測試代碼:

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

固然他仍然是支持鏈式調用的:

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

這樣來寫代碼,真的很簡潔,而且可讀性很強。

使用 builder

Builder 模式我不想再多解釋了,讀者能夠看一下《Head First》(設計模式) 的建造者模式。

今天其實要說的是一種變種的 builder 模式,那就是構建 bean 的 builder 模式,其實主要的思想是帶着你們一塊兒看一下 lombok 給咱們帶來了什麼。

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

public class Student {
    private String name;
    private int age;

    public String getName() {
            return name;
    }

    public void setName(String name) {
            this.name = name;
    }

    public int getAge() {
            return age;
    }

    public void setAge(int age) {
            this.age = age;
    }

    public static Builder builder(){
            return new Builder();
    }
    public static class Builder{
            private String name;
            private int age;
            public Builder name(String name){
                    this.name = name;
                    return this;
            }

            public Builder age(int age){
                    this.age = age;
                    return this;
            }

            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 class Student {
    private String name;
    private int age;
}

調用方式:

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

代理模式

正如咱們所知的,在程序中調用 rest 接口是一個常見的行爲動做,若是你和我同樣使用過 Spring 的RestTemplate,我相信你會我和同樣,對他拋出的非 http 狀態碼異常深惡痛絕。

因此咱們考慮將RestTemplate最爲底層包裝器進行包裝器模式的設計:

public abstract class FilterRestTemplate implements RestOperations {
        protected volatile RestTemplate restTemplate;

        protected FilterRestTemplate(RestTemplate restTemplate){
                this.restTemplate = restTemplate;
        }

        //實現RestOperations全部的接口
}

而後再由擴展類對FilterRestTemplate進行包裝擴展:

public class ExtractRestTemplate extends FilterRestTemplate {
    private RestTemplate restTemplate;
    public ExtractRestTemplate(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全部的接口」,這個操做絕對不是一時半會能夠寫完的,當時在重構以前我幾乎寫了半個小時,以下:

public abstract class FilterRestTemplate implements RestOperations {

    protected volatile RestTemplate restTemplate;

    protected FilterRestTemplate(RestTemplate restTemplate) {
            this.restTemplate = restTemplate;
    }

    @Override
    public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
            return restTemplate.getForObject(url,responseType,uriVariables);
    }

    @Override
    public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
            return restTemplate.getForObject(url,responseType,uriVariables);
    }

    @Override
    public <T> 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
public abstract class FilterRestTemplate implements RestOperations {
    @Delegate
    protected volatile RestTemplate restTemplate;
}

這幾行代碼徹底替代上述那些冗長的代碼。
是否是很簡潔,作一個擁抱 lombok 的程序員吧。

重構

需求案例

項目需求

項目開發階段,有一個關於下單發貨的需求:若是今天下午 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 代碼的程序員了。

技能

UML

不想多討論 UML 相關的知識,可是我以爲你若是真的會寫 java,請先學會表達本身,UML 就是你說話的語言,作一名優秀的 java 程序員,請至少學會這兩種 UML 圖:

  1. 類圖
  2. 時序圖

clean code

我認爲保持代碼的簡潔和可讀性是代碼的最基本保證,若是有一天爲了程序的效率而下降了這兩點,我認爲是能夠諒解的,除此以外,沒有任何理由可讓你任意揮霍你的代碼。

  1. 讀者能夠看一下 Robert C. Martin 出版的《Clean Code》(代碼整潔之道) 這本書
  2. 能夠參考美團文章聊聊 clean codehttp://tech.meituan.com/clean...
  3. 也能夠看一下阿里的Java 編碼規範https://yq.aliyun.com/article...

不管如何,請保持你的代碼的整潔。

linux 基礎命令

這點其實和會寫 java 沒有關係,可是 linux 不少時候確實承載運行 java 的容器,請學好 linux 的基礎命令。

  1. 參考鳥哥的《Linux 私房菜》

總結

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

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

我但願看到這篇文章的各位都能作一個優秀的 java 程序員。

開源項目推薦

做者的其餘開源項目推薦:

  1. springboot-guide : 適合新手入門以及有經驗的開發人員查閱的 Spring Boot 教程(業餘時間維護中,歡迎一塊兒維護)。
  2. programmer-advancement : 我以爲技術人員應該有的一些好習慣!
  3. spring-security-jwt-guide :從零入門 !Spring Security With JWT(含權限驗證)後端部分代碼。

公衆號

個人公衆號

相關文章
相關標籤/搜索