工作一年,維護工程項目的同時一直寫CURD,最近學習DDD,結合之前自己寫的開源項目,深思我們這種CURD的編程方式的弊端,和朋友討論後,發現我們從來沒有面向對象開發,所以寫這篇文章,希望更多人去思考面向對象,不只是停留在背書上 下麵以開發一個常規的登錄模塊為例,模擬實現一個登錄功能,一步步地去說明 ...
工作一年,維護工程項目的同時一直寫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的過程式代碼