Web 開發選 MVC 還是 DDD?
距離上次更新已經有 7 天了,只要停下來一天,就會有第二天,第三天,越是不寫,就越不知道寫什么。這就是慣性的力量,無論是勤勞還是懶惰,都會產生慣性,于是勤勞者越來越勤勞,懶惰者越來越懶惰,學霸越來越霸,學渣越來越渣。時間一長,就會覺得自己根本無法改變自己,總會回到我們習以為常的狀態。所以,朋友們,一定要警惕慣性,他使我們越來越好,也會使我們越來越壞,這不,我又逼著自己更新了。
之所以停止了更新,一方面是懶惰的小人擊敗了勤奮,另一方面是因為時間不夠用。下班后就那么點時間,用于這個就不能用于那個,而我又是一個喜歡寫代碼的人,一旦開始寫代碼,時間就飛快的流失,從晚上 8 點寫到晚上 12 點,也是一眨眼的功夫,明天還要上班,我不可能再熬夜。熬夜會把第二天廢掉,得不償失。最近在學習并嘗試 golang 的 Web 開發,已經入門了,從以前 Django 的 MVC 模式,也漸漸的切換到了 Golang 的 DDD 模式,感覺 DDD 更具有面向對象風格,而 MVC 更像是一種面向過程的風格。
今天展開來說,什么是 MVC,什么是 DDD,它們分別適合什么樣的場景。
什么是 MVC、什么是 DDD
MVC 三層架構中的 M 表示 Model,V 表示 View,C 表示 Controller。它將整個項目分為三層:展示層、邏輯層、數據層。熟悉 Django 的朋友可以這么映射,M 就是我們編寫的 models.py 表示數據層,定義數據的存儲,而 V 就是 views.py,里面存放著大量的業務邏輯,C 就是 urls.py 控制著路由的訪問。前端請求首先訪問 Controller,然后是 View,最后是 Model,這就是面向數據訪問的過程來定義的架構。
MVC 的缺點就是雖然 M 和 V 是兩份文件,但是數據和業務邏輯高度耦合的,也就是說,M 只負責了數據的定義,而數據的操作都在 V,一旦修改了 M,改 V 是真是苦不堪言,這種將數據與操作分離的特點,破壞了面向對象的封裝特性,是一種典型的面向過程的編程風格。
與之對應,將數據和操作定義在一起,就是 DDD,全稱叫領域驅動設計(Domain Driven Design,簡稱 DDD),領域驅動設計這個概念并不新穎,早在 2004 年就被提出了,不過現在又被大家重視起來,還是基于微服務的興起,微服務就是大服務拆分為小服務嘛,這樣就要做好業務模塊劃分,自然也就加速了領域驅動設計的盛行。
DDD 開發模式實現的代碼,也是按照 MVC 三層架構分層的。Controller 層還是負責暴露 API 接口,M 層還是負責數據存取,V 層負責核心業務邏輯。它跟 MVC 的主要區別還是 M 和 V 的不同。傳統的 M 只定義數據數據的結構,不定義數據的操作,而 DDD 開發模式,M 不僅定義數據的結構,還定義數據的操作。
比如 Django 的 M 和 V 可能是這樣的:
M
# models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
# 數據模型的定義
class User(AbstractUser):
"""
數據的定義
"""
...
class Meta:
db_table = 'user'
verbose_name = '用戶信息'
verbose_name_plural = verbose_name
M
# views.py
class UserViewSet(viewsets.ModelViewSet):
"""
數據的操作、增刪改查
"""
...
Golang 的 M:
// User.go
type User struct {
//數據的定義
...
}
//數據的操作、增刪改查
func (u *User) BeforeSave() error {
...
}
func (u *User) Prepare() {
...
}
func (u *User) Save(db *gorm.DB) (*User, error) {
...
}
func (u *User) UpdateAUser(db *gorm.DB, uid uint32) (*User, error) {
...
}
func (u *User) DeleteAUser(db *gorm.DB, uid uint32) (int64, error) {
...
}
Golang 的 V
func (server *Server) DeleteUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
user := models.User{}
uid, err := strconv.ParseUint(vars["id"], 10, 32)
if err != nil {
responses.ERROR(w, http.StatusBadRequest, err)
return
}
tokenID, err := auth.ExtractTokenID(r)
if err != nil {
responses.ERROR(w, http.StatusUnauthorized, errors.New("Unauthorized"))
return
}
if tokenID != 0 && tokenID != uint32(uid) {
responses.ERROR(w, http.StatusUnauthorized, errors.New(http.StatusText(http.StatusUnauthorized)))
return
}
_, err = user.DeleteAUser(server.DB, uint32(uid))
if err != nil {
responses.ERROR(w, http.StatusInternalServerError, err)
return
}
w.Header().Set("Entity", fmt.Sprintf("%d", uid))
responses.JSON(w, http.StatusNoContent, "")
}
里面調用了 M 中的 DeleteAUser,以后修改 Model 時,只需要修改函數 DeleteAUser,而不需要修改 V。
注意,MVC 和 DDD 與編程語言和框架都沒有關系,因為正好手邊有對應的代碼,就拿來用了。
MVC 和 DDD 分別適合什么樣的場景?
MVC 適合簡單的業務,DDD 適合復雜的業務,為什么這么說呢?
如果系統業務比較簡單,簡單到就是基于 SQL 的 CRUD 操作,那么根本不需要動腦子精心設計 DDD 模型,MVC 模型就足以應付這種簡單業務的開發工作。因為業務比較簡單,即便我們使用 DDD,那模型本身包含的業務邏輯也并不會很多,設計出來的領域模型也會比較單薄,跟 MVC 差不多,沒有太大意義。
你可能會問,DDD 不就是把部分數據的操作放在了模型里面嗎,為什么就適合復雜的業務呢?
不夸張地講,MVC 模式的開發,大部分都是 SQL 驅動(SQL-Driven)的開發模式。我們接到一個后端接口的開發需求的時候,就去看接口需要的數據對應到數據庫中,需要哪張表或者哪幾張表,然后思考如何編寫 SQL 語句來獲取數據。之后就是定義 models.py 編寫 views.py 中的視圖函數,你可以這么理解,views.py 中就是各種 SQL 語句。而 SQL 語句是不能復用的,新接口開發即使有部分相同的邏輯,也只能重新編寫視圖函數。
而 DDD 開發模式下,我們需要事先理清楚所有的業務,定義領域模型所包含的屬性和方法。領域模型相當于可復用的業務中間層。新功能需求的開發,都基于之前定義好的這些領域模型來完成。越復雜的系統,對代碼的復用性、易維護性要求就越高,我們就越應該花更多的時間和精力在前期設計上。DDD 開發模式,正好需要我們前期做大量的業務調研、領域模型設計,所以它更加適合這種復雜系統的開發。
最后的話
平時做 Web 開發,基本上,都是使用 MVC 架構,就連 Spring 的官方 Demo 也是 MVC 模式,也就是說 MVC 仍然是主流,因為項目之前就是 MVC 架構,保持不變的成本最小。
但 MVC 是典型的面向過程風格的設計,不適合復雜的系統,比如金融類系統、賬務核算系統。DDD 架構把數據和操作封裝在一起,對數據的操作可以復用,是面向對象風格的設計,比較適合復雜的業務系統。
一句話,簡單的系統,就用 MVC,復雜的系統就用 DDD。