如何寫工程程式碼——重新認識物件導向

暮夜望日發表於2019-05-01

工作一年,維護工程專案的同時一直寫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的過程式程式碼

相關文章