設計模式之美學習(九):業務開發經常使用的基於貧血模型的MVC架構違背OOP嗎?

咱們都知道,不少業務系統都是基於 MVC 三層架構來開發的。實際上,更確切點講,這是一種基於貧血模型的 MVC 三層架構開發模式。前端

雖然這種開發模式已經成爲標準的 Web 項目的開發模式,但它卻違反了面向對象編程風格,是一種不折不扣的面向過程的編程風格,所以而被有些人稱爲反模式(anti-pattern)。特別是領域驅動設計Domain Driven Design,簡稱 DDD)盛行以後,這種基於貧血模型的傳統的開發模式就更加被人詬病。而基於充血模型的 DDD 開發模式愈來愈被人提倡。java

基於上面的描述,咱們先搞清楚下面幾個問題:程序員

  • 什麼是貧血模型?什麼是充血模型?
  • 爲何說基於貧血模型的傳統開發模式違反 OOP?
  • 基於貧血模型的傳統開發模式既然違反 OOP,那又爲何如此流行?
  • 什麼狀況下咱們應該考慮使用基於充血模型的 DDD 開發模式?

什麼是基於貧血模型的傳統開發模式?

對於大部分的後端開發工程師來講,MVC 三層架構都不會陌生。數據庫

MVC 三層架構中的 M 表示 ModelV 表示 ViewC 表示 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 後端項目的時候,基本上都是這麼組織代碼的。其中,UserEntityUserRepository 組成了數據訪問層,UserBoUserService 組成了業務邏輯層,UserVoUserController 在這裏屬於接口層。框架

從代碼中能夠發現,UserBo 是一個純粹的數據結構,只包含數據,不包含任何業務邏輯。業務邏輯集中在 UserService 中。咱們經過 UserService 來操做 UserBo。換句話說,Service 層的數據和業務邏輯,被分割爲 BOService 兩個類中。像 UserBo 這樣,只包含數據,不包含業務邏輯的類,就叫做貧血模型Anemic Domain Model)。同理,UserEntityUserVo 都是基於貧血模型設計的。這種貧血模型將數據與操做分離,破壞了面向對象的封裝特性,是一種典型的面向過程的編程風格。前後端分離

什麼是基於充血模型的 DDD 開發模式?

首先,咱們先來看一下,什麼是充血模型?

在貧血模型中,數據和業務邏輯被分割到不一樣的類中。充血模型Rich Domain Model)正好相反,數據和對應的業務邏輯被封裝到同一個類中。所以,這種充血模型知足面向對象的封裝特性,是典型的面向對象編程風格。

接下來,再來看一下,什麼是領域驅動設計?

領域驅動設計,即 DDD,主要是用來指導如何解耦業務系統,劃分業務模塊,定義業務領域模型及其交互。領域驅動設計這個概念並不新穎,早在 2004 年就被提出了,到如今已經有十幾年的歷史了。不過,它被大衆熟知,仍是基於另外一個概念的興起,那就是微服務。

除了監控、調用鏈追蹤、API 網關等服務治理系統的開發以外,微服務還有另一個更加劇要的工做,那就是針對公司的業務,合理地作微服務拆分。而領域驅動設計剛好就是用來指導劃分服務的。因此,微服務加速了領域驅動設計的盛行。

領域驅動設計有點兒相似敏捷開發、SOAPAAS 等概念,聽起來很高大上,但實際上只值「五分錢」。即使你沒有據說過領域驅動設計,對這個概念一無所知,只要你是在開發業務系統,也或多或少都在使用它。作好領域驅動設計的關鍵是,看你對本身所作業務的熟悉程度,而並非對領域驅動設計這個概念自己的掌握程度。即使你對領域驅動搞得再清楚,可是對業務不熟悉,也並不必定能作出合理的領域設計。因此,不要把領域驅動設計當銀彈,不要花太多的時間去過分地研究它。

實際上,基於充血模型的 DDD 開發模式實現的代碼,也是按照 MVC 三層架構分層的。Controller 層仍是負責暴露接口,Repository 層仍是負責數據存取,Service 層負責核心業務邏輯。它跟基於貧血模型的傳統開發模式的區別主要在 Service 層。

在基於貧血模型的傳統開發模式中,Service 層包含 Service 類和 BO 類兩部分,BO 是貧血模型,只包含數據,不包含具體的業務邏輯。業務邏輯集中在 Service 類中。在基於充血模型的 DDD 開發模式中,Service 層包含 Service 類和 Domain 類兩部分。Domain 就至關於貧血模型中的 BO。不過,DomainBO 的區別在於它是基於充血模型開發的,既包含數據,也包含業務邏輯。而 Service 類變得很是單薄。總結一下的話就是,基於貧血模型的傳統的開發模式,重 ServiceBO;基於充血模型的 DDD 開發模式,輕 ServiceDomain

爲何基於貧血模型的傳統開發模式如此受歡迎?

基於貧血模型的傳統開發模式,將數據與業務邏輯分離,違反了 OOP 的封裝特性,其實是一種面向過程的編程風格。可是,如今幾乎全部的 Web 項目,都是基於這種貧血模型的開發模式,甚至連 Java Spring 框架的官方 demo,都是按照這種開發模式來編寫的。

面向過程編程風格有種種弊端,好比,數據和操做分離以後,數據自己的操做就不受限制了。任何代碼均可以隨意修改數據。既然基於貧血模型的這種傳統開發模式是面向過程編程風格的,那它又爲何會被廣大程序員所接受呢?

第一點緣由是,大部分狀況下,咱們開發的系統業務可能都比較簡單,簡單到就是基於 SQLCRUD 操做,因此,咱們根本不須要動腦子精心設計充血模型,貧血模型就足以應付這種簡單業務的開發工做。除此以外,由於業務比較簡單,即使咱們使用充血模型,那模型自己包含的業務邏輯也並不會不少,設計出來的領域模型也會比較單薄,跟貧血模型差很少,沒有太大意義。

第二點緣由是,充血模型的設計要比貧血模型更加有難度。由於充血模型是一種面向對象的編程風格。咱們從一開始就要設計好針對數據要暴露哪些操做,定義哪些業務邏輯。而不是像貧血模型那樣,咱們只須要定義數據,以後有什麼功能開發需求,咱們就在 Service 層定義什麼操做,不須要事先作太多設計。

第三點緣由是,思惟已固化,轉型有成本。基於貧血模型的傳統開發模式經歷了這麼多年,已經深得人心、習覺得常。你隨便問一個旁邊的大齡同事,基本上他過往參與的全部 Web 項目應該都是基於這個開發模式的,並且也沒有出過啥大問題。若是轉向用充血模型、領域驅動設計,那勢必有必定的學習成本、轉型成本。不少人在沒有遇到開發痛點的狀況下,是不肯意作這件事情的。

什麼項目應該考慮使用基於充血模型的 DDD 開發模式?

基於貧血模型的傳統的開發模式,比較適合業務比較簡單的系統開發。相對應的,基於充血模型的 DDD 開發模式,更適合業務複雜的系統開發。好比,包含各類利息計算模型、還款模型等複雜業務的金融系統。

這兩種開發模式,落實到代碼層面,區別不就是一個將業務邏輯放到 Service 類中,一個將業務邏輯放到 Domain 領域模型中嗎?爲何基於貧血模型的傳統開發模式,就不能應對複雜業務系統的開發?而基於充血模型的 DDD 開發模式就能夠呢?

實際上,除了咱們能看到的代碼層面的區別以外(一個業務邏輯放到 Service 層,一個放到領域模型中),還有一個很是重要的區別,那就是兩種不一樣的開發模式會致使不一樣的開發流程。基於充血模型的 DDD 開發模式的開發流程,在應對複雜業務系統的開發的時候更加有優點。爲何這麼說呢?先來回憶一下,咱們平時基於貧血模型的傳統的開發模式,都是怎麼實現一個功能需求的。

不誇張地講,咱們平時的開發,大部分都是 SQL 驅動(SQL-Driven)的開發模式。咱們接到一個後端接口的開發需求的時候,就去看接口須要的數據對應到數據庫中,須要哪張表或者哪幾張表,而後思考如何編寫 SQL 語句來獲取數據。以後就是定義 EntityBOVO,而後模板式地往對應的 RepositoryServiceController 類中添加代碼。

業務邏輯包裹在一個大的 SQL 語句中,而 Service 層能夠作的事情不多。SQL 都是針對特定的業務功能編寫的,複用性差。當我要開發另外一個業務功能的時候,只能從新寫個知足新需求的 SQL 語句,這就可能致使各類長得差很少、區別很小的 SQL 語句滿天飛。

因此,在這個過程當中,不多有人會應用領域模型、OOP 的概念,也不多有代碼複用意識。對於簡單業務系統來講,這種開發方式問題不大。但對於複雜業務系統的開發來講,這樣的開發方式會讓代碼愈來愈混亂,最終致使沒法維護。

若是咱們在項目中,應用基於充血模型的 DDD 的開發模式,那對應的開發流程就徹底不同了。在這種開發模式下,咱們須要事先理清楚全部的業務,定義領域模型所包含的屬性和方法。領域模型至關於可複用的業務中間層。新功能需求的開發,都基於以前定義好的這些領域模型來完成。

越複雜的系統,對代碼的複用性、易維護性要求就越高,咱們就越應該花更多的時間和精力在前期設計上。而基於充血模型的 DDD 開發模式,正好須要咱們前期作大量的業務調研、領域模型設計,因此它更加適合這種複雜系統的開發。

重點回顧

平時作 Web 項目的業務開發,大部分都是基於貧血模型的 MVC 三層架構,這裏把它稱爲傳統的開發模式。之因此稱之爲「傳統」,是相對於新興的基於充血模型的 DDD 開發模式來講的。基於貧血模型的傳統開發模式,是典型的面向過程的編程風格。相反,基於充血模型的 DDD 開發模式,是典型的面向對象的編程風格。

不過,DDD 也並不是銀彈。對於業務不復雜的系統開發來講,基於貧血模型的傳統開發模式簡單夠用,基於充血模型的 DDD 開發模式有點大材小用,沒法發揮做用。相反,對於業務複雜的系統開發來講,基於充血模型的 DDD 開發模式,由於前期須要在設計上投入更多時間和精力,來提升代碼的複用性和可維護性,因此相比基於貧血模型的開發模式,更加有優點。

思考

  • 對於舉的例子中,UserEntityUserBoUserVo 包含的字段都差很少,是否能夠合併爲一個類呢?

參考:實戰一(上):業務開發經常使用的基於貧血模型的MVC架構違背OOP嗎?

本文由博客一文多發平臺 OpenWrite 發佈!
更多內容請點擊個人博客 沐晨

相關文章
相關標籤/搜索