工作一年,維護工程專案的同時一直寫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();
//登入後續
}
複製程式碼
這樣看是好看很多,但是換湯不換藥,維護過工程專案的同學都會發現,專案裡基本都是這種程式碼,維護起來成本極高:
-
login方法被抽成幾個方法,login方法是簡單了,service卻臃腫了
-
service臃腫後開始拆分service,再不濟開始建立多一層manage之類的
-
複用極其困難,因為checkUserStatus這種方法往往是私有,並且這種抽離對其它業務場景是否合適也不好說
-
在程式碼開始出現冗餘時,會開始寫一些帶有業務邏輯的Utils,把汙染擴散到Utils
-
由於複用極其困難,開始出現多個類似功能的方法,分佈在不同類裡,後繼維護專案的人很難分清類似方法的區別
-
因為不好統一表達語義,DTO等物件會在service層氾濫,controller和service耦合嚴重,導致分層變得沒有意義
-
1,2其實是一個死迴圈,最後直接反映到專案難以維護上
-
在多資料來源,多事務的情況下,難以確定事務邊界,容易出現事務不能回滾的情況
-
單元測試的編寫是個噩夢,嘗試寫單測的同學應該深有體會
為什麼會這樣呢?因為我們到這裡為止,依然還是程式導向程式設計,完全沒有物件導向的思維。程式碼其實都是堆起來,責任和邊界不清晰,導致複用很難,維護變更的成本很高,所以專案經過多人維護後會變得更嚴重。唯一像物件導向的程式碼就是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的過程式程式碼