如何寫工程代碼——重新認識面向對象
工作一年,維護工程項目的同時一直寫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的過程式代碼
如何寫工程代碼——重新認識面向對象