為什么從 MVC 到 DDD,架構的本質是什么?
今天要分享的是 MVC 和 DDD 的架構本質,通過由淺入深的介紹講解和視頻帶著手把手操作創(chuàng)建工程架構。讓無論是學習 MVC 的小白碼農還是希望了解更多關于 DDD 內容的老白碼農,都可以學習到一點自己需要的內容。
一、MVC 架構
如果我們嘗試把編程的復雜架構縮小到最容易理解的程度,那么編程開發(fā)其實只做3件事:”定義屬性、創(chuàng)建方法、調用展示“。但因為同類所需的內容較多,如一系列的屬性,一堆的方法實現,一組的接口封裝,那么就需要合理的把這些內容分配到不同的層次中去實現,因此有了分層架構的設計。
那么本文小傅哥會向大家介紹一套MVC架構的分層設計以及如何創(chuàng)建使用,并提供相應的簡單的案例。你可以復制這套架構在自己的場景中使用,也更能方便編程的小白可以更快的上手開發(fā)。
注意:此套MVC架構模型適合提供HTTP服務的工程架構,適合簡單的小場景開發(fā)使用。特點;輕便、簡單、學習成本低。
1. 編程三步
如果說你是一個特別小的玩具項目,你甚至可以把編程的3步寫到一個類里。但因為你做的是正經項目,你的各種類;對象類、庫表類、方法類,就會成群結隊的來。如果你想把這些成群結隊的類的內容,都寫到一個類里去,那么就是幾萬行的代碼了?!?當然你也可以吹牛逼,你一個人做過一個項目,這項目大到啥程度呢。就是有一個類里有上萬行代碼。
圖片
所以,為了不至于讓一個類撐到爆??,需要把黃色的對象、綠色的方法、紅色的接口,都分配到不同的包結構下。這就是你編碼人生中所接觸到的第一個解耦操作。
2. 分層框架
MVC 是一種非常常見且常用的分層架構,主要包括;M - mode 對象層,封裝到 domain 里。V - view 展示層,但因為目前都是前后端分離的項目,幾乎不會在后端項目里寫 JSP 文件了。C - Controller 控制層,對外提供接口實現類。DAO 算是單獨拿出來用戶處理數據庫操作的層。
圖片
- 如圖,在 MVC 的分層架構下。我們編程3步的所需各類對象、方法、接口,都分配到 MVC 的各個層次中去。
- 因為這樣分層以后,就可以很清晰明了的知道各個層都在做什么內容,也更加方便后續(xù)的維護和迭代。
- 對于一個真正的項目來說,是沒有一錘子買賣的,最開始的開發(fā)遠不是成本所在。最大的開發(fā)成本是后期的維護和迭代。而架構設計的意義更多的就是在解決系統(tǒng)的反復的維護和迭代時,如何降低成本,這也是架構分層的意義所在。
3. 調用流程
接下來我們再看下一套 MVC 架構中各個模塊在調用時的串聯關系;
圖片
- 以用戶發(fā)起 HTTP 請求開始,Controller 在接收到請求后,調用由 Spring 注入到類里的 Service 方法,進入 Service 方法后有些邏輯會走數據庫,有些邏輯是直接內部自己處理后就直接返回給 Controller 了。最后由 Controller 封裝結果返回給 HTTP 響應。
- 同時我們也可以看到各個對象在這些請求間的一個作用,如;請求對象、庫表對象、返回對象。
4. 架構源碼
4.1 環(huán)境
- JDK 1.8
- Maven 3.8.6 - 下載安裝maven后,本地記得配置阿里云鏡像,方便快速拉取jar包。源碼中 docs/maven/settings.xml 有阿里云鏡像地址。
- SpringBoot 2.7.2
- MySQL 5.7 - 如果你使用 8.0 記得更改 pom.xml 中的 mysql 引用
4.2 架構
- 源碼:https://gitcode.net/KnowledgePlanet/road-map/xfg-frame-mvc
- 樹形:安裝 brew install tree IntelliJ IDEA Terminal 使用 tree
.
├── docs
│ └── mvc.drawio - 架構文檔
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── cn
│ │ │ └── bugstack
│ │ │ └── xfg
│ │ │ └── frame
│ │ │ ├── Application.java
│ │ │ ├── common
│ │ │ │ ├── Constants.java
│ │ │ │ └── Result.java
│ │ │ ├── controller
│ │ │ │ └── UserController.java
│ │ │ ├── dao
│ │ │ │ └── IUserDao.java
│ │ │ ├── domain
│ │ │ │ ├── po
│ │ │ │ │ └── User.java
│ │ │ │ ├── req
│ │ │ │ │ └── UserReq.java
│ │ │ │ ├── res
│ │ │ │ │ └── UserRes.java
│ │ │ │ └── vo
│ │ │ │ └── UserInfo.java
│ │ │ └── service
│ │ │ ├── IUserService.java
│ │ │ └── impl
│ │ │ └── UserServiceImpl.java
│ │ └── resources
│ │ ├── application.yml
│ │ └── mybatis
│ │ ├── config
│ │ │ └── mybatis-config.xml
│ │ └── mapper
│ │ └── User_Mapper.xml
│ └── test
│ └── java
│ └── cn
│ └── bugstack
│ └── xfg
│ └── frame
│ └── test
│ └── ApiTest.java
└── road-map.sql
以上是整個??工程架構的 tree 樹形圖。整個工程由 SpringBoot 驅動。
- Application.java 是啟動程序的 SpringBoot 應用
- common 是額外添加的一個層,用于定義通用的類
- controller 控制層,提供接口實現。
- dao 數據庫操作層
- domain 對象定義層
- service 服務實現層
5. 測試驗證
- 首先;整個工程由 SpringBoot 驅動,提供了 road-map.sql 測試 SQL 庫表語句。你可以在自己的本地mysql上進行執(zhí)行。它會創(chuàng)建庫表。
- 之后;在 application.yml 配置數據庫鏈接信息。
- 之后就可以打開 ApiTest 進行測試了。你可以點擊 Application 類的綠色箭頭啟動工程,使用 UserController 類提供接口的方式調用程序;http://localhost:8089/queryUserInfo
圖片
- 如果你正常獲取了這樣的結果信息,那么說明你已經啟動成功。接下來就可以對照著MVC的結構進行學習,以及使用這樣的工程結構開發(fā)自己的項目。
二、DDD 架構
從最早接觸 DDD 架構,到后來用 DDD 架構不斷的承接項目開發(fā),一次次在項目開發(fā)中的經驗積累。對 DDD 有了不少的理解。DDD 是一種思想,落地的形態(tài)和結構會有不同的方式,甚至在編碼上也會有風格的差異。但終期目標就一個;”提供代碼的可維護性,降低迭代開發(fā)成本?!耙彩强低伤觯骸比魏谓M織在設計一套系統(tǒng)時,所交付的設計方案在結構上都與該組織的溝通結構保持一致?!?/p>
但 DDD 與 MVC 相比的概率較多,貿然用理論驅動代碼開發(fā),會讓整個工程變得非?;靵y,甚至可能雖然是用的 DDD 但最后寫出來了一片四不像的 MVC 代碼。所以對于程序員??????來說,先能上手一個工程,在從工程了解理論會更加容易。為此小傅哥想以此文,通過實戰(zhàn)編碼的方式向大家分享 DDD 架構,并能讓大家上手的 DDD 架構。
1. 問題碰撞
你用 MVC 寫代碼,遇到過最大的問題是什么???
簡單、容易、好理解,是 MVC 架構的特點,但也正因為簡單的分層邏輯,在適配較復雜的場景并且需要長周期的維護時,代碼的迭代成本就會越來越高。如圖;
圖片
- 如果你接觸過較大型且已經長期維護項目的 MVC 架構,你就會發(fā)現這里的 DAO、PO、VO 對象,在 Service 層相互調用。那么長期開發(fā)后,就導致了各個 PO 里的屬性字段數量都被撐的特別大。這樣的開發(fā)方式,將”狀態(tài)”、“行為“分離到不同的對象中,代碼的意圖漸漸模糊,膨脹、臃腫和不穩(wěn)定的架構,讓迭代成本增加。
- 而 DDD 架構首先以解決此類問題為主,將各個屬于自己領域范圍內的行為和邏輯封裝到自己的領域包下處理。這也是 DDD 架構設計的精髓之一。它希望在分治層面合理切割問題空間為更小規(guī)模的若干子問題,而問題越小就容易被理解和處理,做到高內聚低耦合。這也是康威定律所提到的,解決復雜場景的設計主要分為:分治、抽象和知識。
2. 簡化理解
在給大家講解 MVC 架構的時候,小傅哥提到了一個簡單的開發(fā)模型。開發(fā)代碼可以理解為:“定義屬性 -> 創(chuàng)建方法 -> 調用展示” 但這個模型結構過于簡單,不太適合運用了各類分布式技術棧以及更多邏輯的 DDD 架構。所以在 DDD 這里,我們把開發(fā)代碼可以抽象為:“觸發(fā) -> 函數 -> 連接” 如圖;
圖片
- DDD 架構常用于微服務場景,因此也一個系統(tǒng)的調用方式就不只是 HTTP 還包括;RPC 遠程、MQ 消息、TASK 任務,因此這些種方式都可以理解為觸發(fā)。
- 通過觸發(fā)調用函數方法,我們這里可以把各個服務都當成一個函數方法來看。而函數方法通過連接,調用到其他的接口、數據庫、緩存來完成函數邏輯。
接下來,小傅哥在帶著大家把這些所需的模塊,拆分到對應的DDD系統(tǒng)架構中。
3. 架構分層
如下是 DDD 架構的一種分層結構,也可以有其他種方式,核心的重點在于適合你所在場景的業(yè)務開發(fā)。以下的分層結構,是小傅哥在使用 DDD 架構多種的方式開發(fā)代碼后,做了簡化和處理的。右側的連線是各個模塊的依賴關系。接下來小傅哥就給大家做一下模塊的介紹。
圖片
- 接口定義 - xfg-frame-api:因為微服務中引用的 RPC 需要對外提供接口的描述信息,也就是調用方在使用的時候,需要引入 Jar 包,讓調用方好能依賴接口的定義做代理。
- 應用封裝 - xfg-frame-app:這是應用啟動和配置的一層,如一些 aop 切面或者 config 配置,以及打包鏡像都是在這一層處理。你可以把它理解為專門為了啟動服務而存在的。
- 領域封裝 - xfg-frame-domain:領域模型服務,是一個非常重要的模塊。無論怎么做DDD的分層架構,domain 都是肯定存在的。在一層中會有一個個細分的領域服務,在每個服務包中會有【模型、倉庫、服務】這樣3部分。
- 倉儲服務 - xfg-frame-infrastructure:基礎層依賴于 domain 領域層,因為在 domain 層定義了倉儲接口需要在基礎層實現。這是依賴倒置的一種設計方式。
- 領域封裝 - xfg-frame-trigger:觸發(fā)器層,一般也被叫做 adapter 適配器層。用于提供接口實現、消息接收、任務執(zhí)行等。所以對于這樣的操作,小傅哥把它叫做觸發(fā)器層。
- 類型定義 - xfg-frame-types:通用類型定義層,在我們的系統(tǒng)開發(fā)中,會有很多類型的定義,包括;基本的 Response、Constants 和枚舉。它會被其他的層進行引用使用。
- 領域編排【可選】 - xfg-frame-case:領域編排層,一般對于較大且復雜的的項目,為了更好的防腐和提供通用的服務,一般會添加 case/application 層,用于對 domain 領域的邏輯進行封裝組合處理。
4. 架構源碼
4.1 環(huán)境
- JDK 1.8
- Maven 3.8.6
- SpringBoot 2.7.2
- MySQL 5.7 - 如果你使用 8.0 記得更改 pom.xml 中的 mysql 引用
4.2 架構
- 源碼:https://gitcode.net/KnowledgePlanet/road-map/xfg-frame-ddd
- 樹形:安裝 brew install tree IntelliJ IDEA Terminal 使用 tree
.
├── README.md
├── docs
│ ├── dev-ops
│ │ ├── environment
│ │ │ └── environment-docker-compose.yml
│ │ ├── siege.sh
│ │ └── skywalking
│ │ └── skywalking-docker-compose.yml
│ ├── doc.md
│ ├── sql
│ │ └── road-map.sql
│ └── xfg-frame-ddd.drawio
├── pom.xml
├── xfg-frame-api
│ ├── pom.xml
│ ├── src
│ │ └── main
│ │ └── java
│ │ └── cn
│ │ └── bugstack
│ │ └── xfg
│ │ └── frame
│ │ └── api
│ │ ├── IAccountService.java
│ │ ├── IRuleService.java
│ │ ├── model
│ │ │ ├── request
│ │ │ │ └── DecisionMatterRequest.java
│ │ │ └── response
│ │ │ └── DecisionMatterResponse.java
│ │ └── package-info.java
│ └── xfg-frame-api.iml
├── xfg-frame-app
│ ├── Dockerfile
│ ├── build.sh
│ ├── pom.xml
│ ├── src
│ │ ├── main
│ │ │ ├── bin
│ │ │ │ ├── start.sh
│ │ │ │ └── stop.sh
│ │ │ ├── java
│ │ │ │ └── cn
│ │ │ │ └── bugstack
│ │ │ │ └── xfg
│ │ │ │ └── frame
│ │ │ │ ├── Application.java
│ │ │ │ ├── aop
│ │ │ │ │ ├── RateLimiterAop.java
│ │ │ │ │ └── package-info.java
│ │ │ │ └── config
│ │ │ │ ├── RateLimiterAopConfig.java
│ │ │ │ ├── RateLimiterAopConfigProperties.java
│ │ │ │ ├── ThreadPoolConfig.java
│ │ │ │ ├── ThreadPoolConfigProperties.java
│ │ │ │ └── package-info.java
│ │ │ └── resources
│ │ │ ├── application-dev.yml
│ │ │ ├── application-prod.yml
│ │ │ ├── application-test.yml
│ │ │ ├── application.yml
│ │ │ ├── logback-spring.xml
│ │ │ └── mybatis
│ │ │ ├── config
│ │ │ │ └── mybatis-config.xml
│ │ │ └── mapper
│ │ │ ├── RuleTreeNodeLine_Mapper.xml
│ │ │ ├── RuleTreeNode_Mapper.xml
│ │ │ └── RuleTree_Mapper.xml
│ │ └── test
│ │ └── java
│ │ └── cn
│ │ └── bugstack
│ │ └── xfg
│ │ └── frame
│ │ └── test
│ │ └── ApiTest.java
│ └── xfg-frame-app.iml
├── xfg-frame-ddd.iml
├── xfg-frame-domain
│ ├── pom.xml
│ ├── src
│ │ └── main
│ │ └── java
│ │ └── cn
│ │ └── bugstack
│ │ └── xfg
│ │ └── frame
│ │ └── domain
│ │ ├── order
│ │ │ ├── model
│ │ │ │ ├── aggregates
│ │ │ │ │ └── OrderAggregate.java
│ │ │ │ ├── entity
│ │ │ │ │ ├── OrderItemEntity.java
│ │ │ │ │ └── ProductEntity.java
│ │ │ │ ├── package-info.java
│ │ │ │ └── valobj
│ │ │ │ ├── OrderIdVO.java
│ │ │ │ ├── ProductDescriptionVO.java
│ │ │ │ └── ProductNameVO.java
│ │ │ ├── repository
│ │ │ │ ├── IOrderRepository.java
│ │ │ │ └── package-info.java
│ │ │ └── service
│ │ │ ├── OrderService.java
│ │ │ └── package-info.java
│ │ ├── rule
│ │ │ ├── model
│ │ │ │ ├── aggregates
│ │ │ │ │ └── TreeRuleAggregate.java
│ │ │ │ ├── entity
│ │ │ │ │ ├── DecisionMatterEntity.java
│ │ │ │ │ └── EngineResultEntity.java
│ │ │ │ ├── package-info.java
│ │ │ │ └── valobj
│ │ │ │ ├── TreeNodeLineVO.java
│ │ │ │ ├── TreeNodeVO.java
│ │ │ │ └── TreeRootVO.java
│ │ │ ├── repository
│ │ │ │ ├── IRuleRepository.java
│ │ │ │ └── package-info.java
│ │ │ └── service
│ │ │ ├── engine
│ │ │ │ ├── EngineBase.java
│ │ │ │ ├── EngineConfig.java
│ │ │ │ ├── EngineFilter.java
│ │ │ │ └── impl
│ │ │ │ └── RuleEngineHandle.java
│ │ │ ├── logic
│ │ │ │ ├── BaseLogic.java
│ │ │ │ ├── LogicFilter.java
│ │ │ │ └── impl
│ │ │ │ ├── UserAgeFilter.java
│ │ │ │ └── UserGenderFilter.java
│ │ │ └── package-info.java
│ │ └── user
│ │ ├── model
│ │ │ └── valobj
│ │ │ └── UserVO.java
│ │ ├── repository
│ │ │ └── IUserRepository.java
│ │ └── service
│ │ ├── UserService.java
│ │ └── impl
│ │ └── UserServiceImpl.java
│ └── xfg-frame-domain.iml
├── xfg-frame-infrastructure
│ ├── pom.xml
│ ├── src
│ │ └── main
│ │ └── java
│ │ └── cn
│ │ └── bugstack
│ │ └── xfg
│ │ └── frame
│ │ └── infrastructure
│ │ ├── dao
│ │ │ ├── IUserDao.java
│ │ │ ├── RuleTreeDao.java
│ │ │ ├── RuleTreeNodeDao.java
│ │ │ └── RuleTreeNodeLineDao.java
│ │ ├── package-info.java
│ │ ├── po
│ │ │ ├── RuleTreeNodeLineVO.java
│ │ │ ├── RuleTreeNodeVO.java
│ │ │ ├── RuleTreeVO.java
│ │ │ └── UserPO.java
│ │ └── repository
│ │ ├── RuleRepository.java
│ │ └── UserRepository.java
│ └── xfg-frame-infrastructure.iml
├── xfg-frame-trigger
│ ├── pom.xml
│ ├── src
│ │ └── main
│ │ └── java
│ │ └── cn
│ │ └── bugstack
│ │ └── xfg
│ │ └── frame
│ │ └── trigger
│ │ ├── http
│ │ │ ├── Controller.java
│ │ │ └── package-info.java
│ │ ├── mq
│ │ │ └── package-info.java
│ │ ├── rpc
│ │ │ ├── AccountService.java
│ │ │ ├── RuleService.java
│ │ │ └── package-info.java
│ │ └── task
│ │ └── package-info.java
│ └── xfg-frame-trigger.iml
└── xfg-frame-types
├── pom.xml
├── src
│ └── main
│ └── java
│ └── cn
│ └── bugstack
│ └── xfg
│ └── frame
│ └── types
│ ├── Constants.java
│ ├── Response.java
│ └── package-info.java
└── xfg-frame-types.iml
以上是整個??工程架構的 tree 樹形圖。整個工程由 xfg-frame-app 模的 SpringBoot 驅動。這里小傅哥在 domain 領域模型下提供了 order、rule、user 三個領域模塊。并在每個模塊下提供了對應的測試內容。這塊是整個模型的重點,其他模塊都可以通過測試看到這里的調用過程。
4.3 領域
一個領域模型中包含3個部分;model、repository、service 三部分;
- model 對象的定義
- repository 倉儲的定義
- service 服務實現
以上3個模塊,一般也是大家在使用 DDD 時候最不容易理解的分層。比如 model 里還分為;valobj - 值對象、entity 實體對象、aggregates 聚合對象;
- 值對象:表示沒有唯一標識的業(yè)務實體,例如商品的名稱、描述、價格等。
- 實體對象:表示具有唯一標識的業(yè)務實體,例如訂單、商品、用戶等;
- 聚合對象:是一組相關的實體對象的根,用于保證實體對象之間的一致性和完整性;
關于model中各個對象的拆分,尤其是聚合的定義,會牽引著整個模型的設計。當然你可以在初期使用 DDD 的時候不用過分在意領域模型的設計,可以把整個 domain 下的一個個包當做充血模型結構,這樣編寫出來的代碼也是非常適合維護的。
4.4 環(huán)境(開發(fā)/測試/上線)
源碼:xfg-frame-ddd/pom.xml
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<profileActive>dev</profileActive>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<profileActive>test</profileActive>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<profileActive>prod</profileActive>
</properties>
</profile>
- 定義環(huán)境;開發(fā)、測試、上線。
源碼:xfg-frame-app/application.yml
spring:
config:
name: xfg-frame
profiles:
active: dev # dev、test、prod
- 除了 pom 的配置,還需要在 application.yml 中指定環(huán)境。這樣就可以對應的加載到;application-dev.yml、application-prod.yml、application-test.yml 這樣就可以很方便的加載對應的配置信息了。尤其是各個場景中切換會更加方便。
4.5 切面
一個工程開發(fā)中,有時候可能會有很多的統(tǒng)一切面和啟動配置的處理,這些內容都可以在 xfg-frame-app 完成。
圖片
源碼:cn.bugstack.xfg.frame.aop.RateLimiterAop
@Slf4j
@Aspect
public class RateLimiterAop {
private final long timeout;
private final double permitsPerSecond;
private final RateLimiter limiter;
public RateLimiterAop(double permitsPerSecond, long timeout) {
this.permitsPerSecond = permitsPerSecond;
this.timeout = timeout;
this.limiter = RateLimiter.create(permitsPerSecond);
}
@Pointcut("execution(* cn.bugstack.xfg.frame.trigger..*.*(..))")
public void pointCut() {
}
@Around(value = "pointCut()", argNames = "jp")
public Object around(ProceedingJoinPoint jp) throws Throwable {
boolean tryAcquire = limiter.tryAcquire(timeout, TimeUnit.MILLISECONDS);
if (!tryAcquire) {
Method method = getMethod(jp);
log.warn("方法 {}.{} 請求已被限流,超過限流配置[{}/秒]", method.getDeclaringClass().getCanonicalName(), method.getName(), permitsPerSecond);
return Response.<Object>builder()
.code(Constants.ResponseCode.RATE_LIMITER.getCode())
.info(Constants.ResponseCode.RATE_LIMITER.getInfo())
.build();
}
return jp.proceed();
}
private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}
}
使用
# 限流配置
rate-limiter:
permits-per-second: 1
timeout: 5
- 這樣你所有的通用配置,又和業(yè)務沒有太大的關系的,就可以直接寫到這里了?!?具體可以參考代碼。
5. 測試驗證
- 首先;整個工程由 SpringBoot 驅動,提供了 road-map.sql 測試 SQL 庫表語句。你可以在自己的本地mysql上進行執(zhí)行。它會創(chuàng)建庫表。
- 之后;在 application.yml 配置數據庫鏈接信息。
- 之后就可以打開 ApiTest 進行測試了。你可以點擊 Application 類的綠色箭頭啟動工程,使用觸發(fā)器里的接口調用測試,或者單元測試RPC接口,小傅哥也提供了泛化調用的方式。
圖片
- 如果你正常獲取了這樣的結果信息,那么說明你已經啟動成功。接下來就可以對照著DDD的結構進行學習,以及使用這樣的工程結構開發(fā)自己的項目。
三、實戰(zhàn) - DDD 項目
紙上得來終覺淺,碼農學習要實戰(zhàn)!
無論是 MVC 還是各類 DDD 所呈現的架構,還是需要看到實際的代碼,以及參與實戰(zhàn)開發(fā)才能更好的吸收。否則都是理論仍舊難以讓人下手。
所以小傅哥為大家準備了一些學習項目,這些項目都是非常具有架構思維以及設計模式的應用級實戰(zhàn)項目架構設計和落地。對于一些小白來說,如果能早早的接觸到這樣的項目,就相當于是提前進入企業(yè)實習了??梢詷O大的提到編程思維以及開發(fā)能力。
這些項目包括:《Lottery 抽獎系統(tǒng) - 基于領域驅動設計的四層架構實踐》、《API網關:中間件設計和落地》、《ChatGPT 微服務應用體系搭建》、《IM 仿微信》、《SpringBoot Starter 中間件設計和落地》等。這里小傅哥只列3張圖,你就知道有多牛皮了!
第1張:Lottery
架構
圖片
工程
圖片
第2張:API網關
架構
圖片
工程
圖片
第3張:ChatGPT