設計原則與思想(二)——MVC與DDD


貧血模型:MVC (Model View-Controller)——反模式(anti-pattern)

充血模型:DDD 領域驅動設計(Domain Driven Design,簡稱 DDD)

MVC貧血模型

MVC 三層架構中的 M 表示 Model,V 表示 View,C 表示 Controller。它將整個項目分為三層:展示層、邏輯層、數據層。

現在很多 Web 或者 App 項目都是前后端分離的,后端負責暴露接口給前端調用。這種情況下,我們一般就將后端項目分為 Repository 層、Service 層、Controller 層。其中,Repository 層負責數據訪問,Service 層負責業務邏輯,Controller 層負責暴露接口。

////////// Controller+VO(View Object) //////////
public class UserController {
  private UserService userService; //通過構造函數或者IOC框架注入
  
  public UserVo getUserById(Long userId) {
    UserBo userBo = userService.getUserById(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;
}
View Code

像 UserBo 這樣,只包含數據,不包含業務邏輯的類,就叫作貧血模型(Anemic Domain Model)。同理,UserEntity、UserVo 都是基於貧血模型設計的。這種貧血模型將數據與操作分離,破壞了面向對象的封裝特性,是一種典型的面向過程的編程風格。

DDD充血模型

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

領域驅動設計,即 DDD,主要是用來指導如何解耦業務系統,划分業務模塊,定義業務領域模型及其交互。

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

實際上,基於充血模型的 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。

  功能 說明
Controller 負責暴露接口 實際上 VO(返回給前端的數據,簡稱VO) 是一種 DTO(Data Transfer Object,數據傳輸對象)。它主要是作為接口的數據傳輸承載體,將數據發送給其他系統。從功能上來講,它理應不包含業務邏輯、只包含數據。
Service 負責核心業務邏輯 貧血模型,包含Service 和 BO(查詢數據庫返回的數據),重 Service 輕 BO(BO 只包含數據,不包含具體的業務邏輯)。
充血模型,包含Service 和 Domain,輕 Service 重 Domain(Domain既包含數據,也包含業務邏輯。而 Service 類變得非常單薄)
Repository 負責數據存取

Repository(倉庫; 貯藏室)

Entity(實體) 的生命周期是有限的。一般來講,我們把它傳遞到 Service 層之后,就會轉化成 BO 或者 Domain 來繼續后面的業務邏輯。Entity 的生命周期到此就結束了

說明:BO、VO、Entity 存在的意義是什么?

針對 Controller、Service、Repository 三層,每層都會定義相應的數據對象,它們分別是 VO(View Object)、BO(Business Object)、Entity,例如 UserVo、UserBo、UserEntity。在實際的開發中,VO、BO、Entity 可能存在大量的重復字段,甚至三者包含的字段完全一樣。在開發的過程中,我們經常需要重復定義三個幾乎一樣的類,顯然是一種重復勞動。

從設計的角度來說,VO、BO、Entity 的設計思路並不違反 DRY 原則,為了分層清晰、減少耦合,多維護幾個類的成本也並不是不能接受的。但是,如果你真的有代碼潔癖,對於代碼重復的問題,我們可以通過繼承或者組合來解決。

如何進行數據對象之間的轉化?最簡單的方式就是手動復制。當然,你也可以使用 Java 中提供了數據對象轉化工具,比如 BeanUtils、Dozer 等,可以大大簡化繁瑣的對象轉化工作。

盡管 VO、BO、Entity 的設計違背 OOP 的封裝特性,有被隨意修改的風險。但 Entity 和 VO 的生命周期是有限的,都僅限在本層范圍內,相對來說是安全的。Service 層包含比較多的業務邏輯代碼,所以 BO 就存在被任意修改的風險了。為了使用方便,我們只能做一些妥協,放棄 BO 的封裝特性,由程序員自己來負責這些數據對象的不被錯誤使用。

總結

基於充血模型的 DDD 開發模式跟基於貧血模型的傳統開發模式相比,主要區別在 Service 層。在基於充血模型的開發模式下,我們將部分原來在 Service 類中的業務邏輯移動到了一個充血的 Domain 領域模型中,讓 Service 類的實現依賴這個 Domain 類。

在基於充血模型的 DDD 開發模式下,Service 類並不會完全移除,而是負責一些不適合放在 Domain 類中的功能。比如,負責與 Repository 層打交道、跨領域模型的業務聚合功能、冪等事務等非功能性的工作。

基於充血模型的 DDD 開發模式跟基於貧血模型的傳統開發模式相比,Controller 層和 Repository 層的代碼基本上相同。這是因為,Repository 層的 Entity 生命周期有限,Controller 層的 VO 只是單純作為一種 DTO。兩部分的業務邏輯都不會太復雜。業務邏輯主要集中在 Service 層。所以,Repository 層和 Controller 層繼續沿用貧血模型的設計思路是沒有問題的。

參考文獻:https://time.geekbang.org/column/article/169600


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM