設計模式之美學習(九):業務開發常用的基於貧血模型的MVC架構違背OOP嗎?
我們都知道,很多業務系統都是基於 MVC
三層架構來開發的。實際上,更確切點講,這是一種基於貧血模型的 MVC
三層架構開發模式。
雖然這種開發模式已經成為標準的 Web
專案的開發模式,但它卻違反了面向物件程式設計風格,是一種徹徹底底的面向過程的程式設計風格,因此而被有些人稱為反模式(anti-pattern
)。特別是領域驅動設計(Domain Driven Design
,簡稱 DDD
)盛行之後,這種基於貧血模型的傳統的開發模式就更加被人詬病。而基於充血模型的 DDD
開發模式越來越被人提倡。
基於上面的描述,我們先搞清楚下面幾個問題:
- 什麼是貧血模型?什麼是充血模型?
- 為什麼說基於貧血模型的傳統開發模式違反
OOP
- 基於貧血模型的傳統開發模式既然違反
OOP
,那又為什麼如此流行? - 什麼情況下我們應該考慮使用基於充血模型的
DDD
開發模式?
什麼是基於貧血模型的傳統開發模式?
對於大部分的後端開發工程師來說,MVC
三層架構都不會陌生。
MVC
三層架構中的 M
表示 Model
,V
表示 View
,C
表示 Controller
。它將整個專案分為三層:展示層、邏輯層、資料層。MVC
三層開發架構是一個比較籠統的分層方式,落實到具體的開發層面,很多專案也並不會 100%
遵從 MVC 固定的分層方式,而是會根據具體的專案需求,做適當的調整。
比如,現在很多 Web
或者 App
專案都是前後端分離的,後端負責暴露介面給前端呼叫。這種情況下,我們一般就將後端專案分為 Repository
Service
層、Controller
層。其中,Repository
層負責資料訪問,Service
層負責業務邏輯,Controller
層負責暴露介面。當然,這只是其中一種分層和命名方式。不同的專案、不同的團隊,可能會對此有所調整。不過,萬變不離其宗,只要是依賴資料庫開發的 Web
專案,基本的分層思路都大差不差。
再來看一下,什麼是貧血模型?
目前幾乎所有的業務後端系統,都是基於貧血模型的。舉一個簡單的例子來解釋一下。
////////// Controller+VO(View Object) ////////// public class UserController { private UserService userService; //通過建構函式或者IOC框架注入 public UserVo getUserById(Long userId) { UserBo userBo = userService.getUser(userId); UserVo userVo = [...convert userBo to userVo...]; return userVo; } } public class UserVo {//省略其他屬性、get/set/construct方法 private Long id; private String name; private String cellphone; } ////////// Service+BO(Business Object) ////////// public class UserService { private UserRepository userRepository; //通過建構函式或者IOC框架注入 public UserBo getUserById(Long userId) { UserEntity userEntity = userRepository.getUserById(userId); UserBo userBo = [...convert userEntity to userBo...]; return userBo; } } public class UserBo {//省略其他屬性、get/set/construct方法 private Long id; private String name; private String cellphone; } ////////// Repository+Entity ////////// public class UserRepository { public UserEntity getUserById(Long userId) { //... } } public class UserEntity {//省略其他屬性、get/set/construct方法 private Long id; private String name; private String cellphone; }
平時開發 Web
後端專案的時候,基本上都是這麼組織程式碼的。其中,UserEntity
和 UserRepository
組成了資料訪問層,UserBo
和 UserService
組成了業務邏輯層,UserVo
和 UserController
在這裡屬於介面層。
從程式碼中可以發現,UserBo
是一個純粹的資料結構,只包含資料,不包含任何業務邏輯。業務邏輯集中在 UserService
中。我們通過 UserService
來操作 UserBo
。換句話說,Service
層的資料和業務邏輯,被分割為 BO
和 Service
兩個類中。像 UserBo
這樣,只包含資料,不包含業務邏輯的類,就叫作貧血模型(Anemic Domain Model
)。同理,UserEntity
、UserVo
都是基於貧血模型設計的。這種貧血模型將資料與操作分離,破壞了面向物件的封裝特性,是一種典型的面向過程的程式設計風格。
什麼是基於充血模型的 DDD 開發模式?
首先,我們先來看一下,什麼是充血模型?
在貧血模型中,資料和業務邏輯被分割到不同的類中。充血模型(Rich Domain Model
)正好相反,資料和對應的業務邏輯被封裝到同一個類中。因此,這種充血模型滿足面向物件的封裝特性,是典型的面向物件程式設計風格。
接下來,再來看一下,什麼是領域驅動設計?
領域驅動設計,即 DDD
,主要是用來指導如何解耦業務系統,劃分業務模組,定義業務領域模型及其互動。領域驅動設計這個概念並不新穎,早在 2004
年就被提出了,到現在已經有十幾年的歷史了。不過,它被大眾熟知,還是基於另一個概念的興起,那就是微服務。
除了監控、呼叫鏈追蹤、API
閘道器等服務治理系統的開發之外,微服務還有另外一個更加重要的工作,那就是針對公司的業務,合理地做微服務拆分。而領域驅動設計恰好就是用來指導劃分服務的。所以,微服務加速了領域驅動設計的盛行。
領域驅動設計有點兒類似敏捷開發、SOA
、PAAS
等概念,聽起來很高大上,但實際上只值“五分錢”。即便你沒有聽說過領域驅動設計,對這個概念一無所知,只要你是在開發業務系統,也或多或少都在使用它。做好領域驅動設計的關鍵是,看你對自己所做業務的熟悉程度,而並不是對領域驅動設計這個概念本身的掌握程度。即便你對領域驅動搞得再清楚,但是對業務不熟悉,也並不一定能做出合理的領域設計。所以,不要把領域驅動設計當銀彈,不要花太多的時間去過度地研究它。
實際上,基於充血模型的 DDD
開發模式實現的程式碼,也是按照 MVC
三層架構分層的。Controller
層還是負責暴露介面,Repository
層還是負責資料存取,Service
層負責核心業務邏輯。它跟基於貧血模型的傳統開發模式的區別主要在 Service
層。
在基於貧血模型的傳統開發模式中,Service
層包含 Service
類和 BO
類兩部分,BO
是貧血模型,只包含資料,不包含具體的業務邏輯。業務邏輯集中在 Service
類中。在基於充血模型的 DDD
開發模式中,Service
層包含 Service
類和 Domain
類兩部分。Domain
就相當於貧血模型中的 BO
。不過,Domain
與 BO
的區別在於它是基於充血模型開發的,既包含資料,也包含業務邏輯。而 Service
類變得非常單薄。總結一下的話就是,基於貧血模型的傳統的開發模式,重 Service
輕 BO
;基於充血模型的 DDD
開發模式,輕 Service
重 Domain
。
為什麼基於貧血模型的傳統開發模式如此受歡迎?
基於貧血模型的傳統開發模式,將資料與業務邏輯分離,違反了 OOP
的封裝特性,實際上是一種面向過程的程式設計風格。但是,現在幾乎所有的 Web
專案,都是基於這種貧血模型的開發模式,甚至連 Java Spring
框架的官方 demo
,都是按照這種開發模式來編寫的。
面向過程程式設計風格有種種弊端,比如,資料和操作分離之後,資料本身的操作就不受限制了。任何程式碼都可以隨意修改資料。既然基於貧血模型的這種傳統開發模式是面向過程程式設計風格的,那它又為什麼會被廣大程式設計師所接受呢?
第一點原因是,大部分情況下,我們開發的系統業務可能都比較簡單,簡單到就是基於 SQL
的 CRUD
操作,所以,我們根本不需要動腦子精心設計充血模型,貧血模型就足以應付這種簡單業務的開發工作。除此之外,因為業務比較簡單,即便我們使用充血模型,那模型本身包含的業務邏輯也並不會很多,設計出來的領域模型也會比較單薄,跟貧血模型差不多,沒有太大意義。
第二點原因是,充血模型的設計要比貧血模型更加有難度。因為充血模型是一種面向物件的程式設計風格。我們從一開始就要設計好針對資料要暴露哪些操作,定義哪些業務邏輯。而不是像貧血模型那樣,我們只需要定義資料,之後有什麼功能開發需求,我們就在 Service
層定義什麼操作,不需要事先做太多設計。
第三點原因是,思維已固化,轉型有成本。基於貧血模型的傳統開發模式經歷了這麼多年,已經深得人心、習以為常。你隨便問一個旁邊的大齡同事,基本上他過往參與的所有 Web
專案應該都是基於這個開發模式的,而且也沒有出過啥大問題。如果轉向用充血模型、領域驅動設計,那勢必有一定的學習成本、轉型成本。很多人在沒有遇到開發痛點的情況下,是不願意做這件事情的。
什麼專案應該考慮使用基於充血模型的 DDD 開發模式?
基於貧血模型的傳統的開發模式,比較適合業務比較簡單的系統開發。相對應的,基於充血模型的 DDD
開發模式,更適合業務複雜的系統開發。比如,包含各種利息計算模型、還款模型等複雜業務的金融系統。
這兩種開發模式,落實到程式碼層面,區別不就是一個將業務邏輯放到 Service
類中,一個將業務邏輯放到 Domain
領域模型中嗎?為什麼基於貧血模型的傳統開發模式,就不能應對複雜業務系統的開發?而基於充血模型的 DDD
開發模式就可以呢?
實際上,除了我們能看到的程式碼層面的區別之外(一個業務邏輯放到 Service
層,一個放到領域模型中),還有一個非常重要的區別,那就是兩種不同的開發模式會導致不同的開發流程。基於充血模型的 DDD
開發模式的開發流程,在應對複雜業務系統的開發的時候更加有優勢。為什麼這麼說呢?先來回憶一下,我們平時基於貧血模型的傳統的開發模式,都是怎麼實現一個功能需求的。
不誇張地講,我們平時的開發,大部分都是 SQL
驅動(SQL-Driven
)的開發模式。我們接到一個後端介面的開發需求的時候,就去看介面需要的資料對應到資料庫中,需要哪張表或者哪幾張表,然後思考如何編寫 SQL
語句來獲取資料。之後就是定義 Entity
、BO
、VO
,然後模板式地往對應的 Repository
、Service
、Controller
類中新增程式碼。
業務邏輯包裹在一個大的 SQL
語句中,而 Service
層可以做的事情很少。SQL
都是針對特定的業務功能編寫的,複用性差。當我要開發另一個業務功能的時候,只能重新寫個滿足新需求的 SQL
語句,這就可能導致各種長得差不多、區別很小的 SQL
語句滿天飛。
所以,在這個過程中,很少有人會應用領域模型、OOP
的概念,也很少有程式碼複用意識。對於簡單業務系統來說,這種開發方式問題不大。但對於複雜業務系統的開發來說,這樣的開發方式會讓程式碼越來越混亂,最終導致無法維護。
如果我們在專案中,應用基於充血模型的 DDD
的開發模式,那對應的開發流程就完全不一樣了。在這種開發模式下,我們需要事先理清楚所有的業務,定義領域模型所包含的屬性和方法。領域模型相當於可複用的業務中間層。新功能需求的開發,都基於之前定義好的這些領域模型來完成。
越複雜的系統,對程式碼的複用性、易維護性要求就越高,我們就越應該花更多的時間和精力在前期設計上。而基於充血模型的 DDD
開發模式,正好需要我們前期做大量的業務調研、領域模型設計,所以它更加適合這種複雜系統的開發。
重點回顧
平時做 Web
專案的業務開發,大部分都是基於貧血模型的 MVC
三層架構,這裡把它稱為傳統的開發模式。之所以稱之為“傳統”,是相對於新興的基於充血模型的 DDD
開發模式來說的。基於貧血模型的傳統開發模式,是典型的面向過程的程式設計風格。相反,基於充血模型的 DDD
開發模式,是典型的面向物件的程式設計風格。
不過,DDD
也並非銀彈。對於業務不復雜的系統開發來說,基於貧血模型的傳統開發模式簡單夠用,基於充血模型的 DDD
開發模式有點大材小用,無法發揮作用。相反,對於業務複雜的系統開發來說,基於充血模型的 DDD
開發模式,因為前期需要在設計上投入更多時間和精力,來提高程式碼的複用性和可維護性,所以相比基於貧血模型的開發模式,更加有優勢。
思考
- 對於舉的例子中,
UserEntity
、UserBo
、UserVo
包含的欄位都差不多,是否可以合併為一個類呢?
參考:實戰一(上):業務開發常用的基於貧血模型的MVC架構違背OOP嗎?
本文由部落格一文多發平臺 OpenWrite 釋出!
更多內容請點選我的部落格 沐晨