1. 程式人生 > >如何寫工程代碼——重新認識面向對象

如何寫工程代碼——重新認識面向對象

持久 org 一個用戶 ons 決定 實現 有效 ont 而不是

工作一年,維護工程項目的同時一直寫CURD,最近學習DDD,結合之前自己寫的開源項目,深思我們這種CURD的編程方式的弊端,和朋友討論後,發現我們從來沒有面向對象開發,所以寫這篇文章,希望更多人去思考面向對象,不只是停留在背書上

下面以開發一個常規的登錄模塊為例,模擬實現一個登錄功能,一步步地去說明其中的弊端和重新解釋面向對象

常規的開發方式

創建模型

@Data
@NoArgsConstructor
class User{
    private Integer Id;
    private String name;
    private String password;//加密過的密碼
  private Integer status;//賬號狀態
}

class UserRepository{
  User getByName(String name);
}

我們都知道mvc,所以會這麽寫

class UserController{
    @RequestMapping("/login")
    public void login(String name,String password){
        userService.login(name,password);
    }
}

class UserService{
    public void login(String name,String password);
}

class UserServiceImpl implements UserService{
    public void login(String name,String password){
        //1.查出這個用戶
        User user = userRepo.getByName(name);
    //2.檢查狀態
    if(user.getStatus()!=1){
      //登錄失敗
    }
    //3.檢查密碼
    if(!Objects.equals(md5(password),user.getPassword())){
        //登錄失敗
    }
    //登錄後續
    }
}

雖然這個login方法有點醜,這還是沒有打點,日誌,生成登錄態的情況下。我們所有的業務都寫在了UserService裏面,可能很多人不覺得這樣寫有什麽問題。如果代碼寫多一點的程序員,可能會把每一步都抽成一個方法

public void login(String name,String password){
  //1.查出這個用戶
  User user = userRepo.getByName(name);
  //2.檢查狀態
  checkUserStatus();
  //3.檢查密碼
  checkPassword();
  //登錄後續
}

這樣看是好看很多,但是換湯不換藥,維護過工程項目的同學都會發現,項目裏基本都是這種代碼,維護起來成本極高:

  1. login方法被抽成幾個方法,login方法是簡單了,service卻臃腫了

  2. service臃腫後開始拆分service,再不濟開始建立多一層manage之類的

  3. 復用極其困難,因為checkUserStatus這種方法往往是私有,並且這種抽離對其它業務場景是否合適也不好說

  4. 在代碼開始出現冗余時,會開始寫一些帶有業務邏輯的Utils,把汙染擴散到Utils

  5. 由於復用極其困難,開始出現多個類似功能的方法,分布在不同類裏,後繼維護項目的人很難分清類似方法的區別

  6. 因為不好統一表達語義,DTO等對象會在service層泛濫,controller和service耦合嚴重,導致分層變得沒有意義

  7. 1,2其實是一個死循環,最後直接反映到項目難以維護上

  8. 在多數據源,多事務的情況下,難以確定事務邊界,容易出現事務不能回滾的情況

  9. 單元測試的編寫是個噩夢,嘗試寫單測的同學應該深有體會

為什麽會這樣呢?因為我們到這裏為止,依然還是面向過程編程,完全沒有面向對象的思維。代碼其實都是堆起來,責任和邊界不清晰,導致復用很難,維護變更的成本很高,所以項目經過多人維護後會變得更嚴重。唯一像面向對象的代碼就是User user = userRepo.getByName(name)這一句了

重新認識面向對象

為什麽說這一句有面向對象的意味?因為這行含義十分明顯,誰做了什麽,我覺得這是一個很好的判斷原則,在scala裏面,是可以把a.do(thing)寫成a do thing主語確定了責任,邊界。在這裏,用戶repo獲取(生成)一個用戶對象。雖然我們一直在說OO,什麽封裝繼承多態,六大原則,張口就來,但是一寫起代碼就變成過程式開發。很多人說設計模式很難學,用不上,很大原因是連對象是什麽都沒概念,還怎麽談面向對象設計

有人會問,上面的User不是對象嗎?這個問題我在學校的時候也被別人問過,當時也覺得很疑惑。當時的問題是這樣的,你覺得上面的User和下面這個有區別嗎

struct User
{
   int id;
   char name[50];
   char password[50];
   int status;
} user;

是的,這是c語言的結構體。你當然不會說這個是對象。這裏有個誤區,我們平時說的Java對象,其實指的是面向對象語言Java裏類的實例,並不等同於面向對象裏的對象。所以上面java對象也不見得是真的OO對象

可以看一下維基百科關於對象的說法

對象是什麽

OO的對象應該是data+behavior,所以我們上面的User對象沒有行為,只是一個數據結構。試想一下,我是用戶,校驗密碼應該是我自己的事,我用什麽加密應該也是我來決定,甚至我加不加密也是我說了算。同樣的,我的狀態應該也是我來管理,我們的User可以改造成這樣

@Data
@NoArgsConstructor
class User{
    private Integer Id;
    private String name;
    private String password;//加密過的密碼
  private Integer status;//賬號狀態
  
  public boolean checkPassword(String pass){
    return Objects.equals(md5(pass),this.password);
  }
  
  public boolean isNormal(){
    return this.status==1
  }
  
  //這裏啰嗦一下,有時候我們不太好把行為寫到數據庫模型類,可以單獨建立一個User類,這個User類也就是DDD裏面的領域對象。如果持久層使用JPA,JPA的數據模型類即是領域對象,JPA允許通過註解去把領域對象綁定到數據模型上。
}

這樣,Service的代碼就簡單很多,只需要關註登錄的邏輯,不需要關心細節

public void login(String name,String password){
  //1.查出這個用戶
  User user = userRepo.getByName(name);
  //2.檢查狀態
  if(!user.isNormal()){
  
  }
  //3.檢查密碼
  if(!user.checkPassword(password)){
  
  }
  //登錄後續
}

這樣做有什麽好處呢

把固有的邏輯由對象本身負責,責任分明,邊界清晰,業務邏輯統一集中,編寫單測更容易

更重要的是,我們的User對象建立起來,有關用戶相關的邏輯,方法,我們可以通過User來表達,並且可以在各個分層中傳遞,統一業務表達語言,可以有效遏制DTO在Service層泛濫的問題。後續會說明一下DTO的問題

理解了對象是什麽後,會更好地反思封裝的重要性,進而深入理解六大原則的含義,開始抽象出接口,在實踐接口的基礎上慢慢地會形成一些手法和技巧,那便是設計模式。而這一切都需要在開發時保持思考,這樣寫是否流程清晰,邊界分明,復用是否容易,最重要的是,是否符合業務的表達,而不是寫出service類do anything的過程式代碼

如何寫工程代碼——重新認識面向對象