新項目模塊不能拆拆拆,但怎么應(yīng)對大型項目?
很多同學(xué)創(chuàng)建一個項目之后,就迫不及待的上手開寫了。項目代碼不像一些框架代碼一樣可以隨意的去寫,但一般都是采用MVC的模式進行開發(fā)。很悲催的是,Java中Web開發(fā)的這些目錄名稱,到現(xiàn)在還是一團亂麻,你需要自己去規(guī)劃。
什么Controller、Service、Dao等,但其實這種劃分方式弊端很多! 本文將先介紹兩種典型的分層結(jié)構(gòu),然后稍微借鑒一下DDD的思想,談一下我在項目中常用的目錄結(jié)構(gòu)。本篇文章非常的實用,將探討怎樣做一個應(yīng)對大型項目的目錄劃分。
清晰的目錄結(jié)構(gòu),能夠輔助其他同學(xué)輕而易舉的了解項目的功能模塊,在項目中保持整體一致的約定也是一個非常好的習(xí)慣。如果再加上一個擴展性,那目錄劃分就是重中之重。
有兩種典型的分類方式,但也有很多細節(jié)。
1. 最簡單的MVC
我們平常最熟悉的,就是MVC結(jié)構(gòu)。這種結(jié)構(gòu)很流行,寫簡單項目很方便,但是會產(chǎn)生嚴(yán)重的耦合問題、Service爆炸問題,數(shù)千、上萬行的代碼是家常便飯。
- Model(模型)表示應(yīng)用程序核心(比如數(shù)據(jù)庫記錄字段)。
- View(視圖)顯示數(shù)據(jù)(數(shù)據(jù)庫記錄)。
- Controller(控制器)處理輸入(寫入數(shù)據(jù)庫記錄)。
在項目劃分上,就類似下面的目錄結(jié)構(gòu)。
1.1 模型
domain是DDD中一個非常寬泛的概念。不過,我們平常就當(dāng)作數(shù)據(jù)庫對應(yīng)的Java 類使用了(沒什么錯)。在實際操作中,它還可能有下面幾種名字,在普通項目中區(qū)別不大,你最好在項目中保持相同的意義來避免歧義。
- entity 這個意義比較明顯,就是實體的意思,最常用。比如JPA的Entity注解
- model模型的意思,一般用來在不同系統(tǒng)之間交互。但如果你的模型非常簡單,直接用entity來表示也是可以的
- domain 這個范圍有點大,甚至?xí)I(lǐng)域內(nèi)service。如果你對DDD的概念不是很熟悉,那就玩上面幾種
對于簡單的項目,我通常在項目中使用entity來表示和數(shù)據(jù)庫的交互。在JPA之類的ORM中,也是做相關(guān)處理的。比如javax.persistence.Entity注解。你要明白的是,Spring Data其實取了一個比較折衷的點,把很多東西揉在一起了。
1.2 Dao
dao層叫數(shù)據(jù)訪問層,全稱為data access object,屬于一種比較底層,比較基礎(chǔ)的操作。在一些其他框架中,還會叫別的名字。
- mapper 這個一般是Mybaits之類的框架所生成的目錄,通常是一些接口。
- repository 倉庫的意思,在jpa中經(jīng)常用。
Dao應(yīng)該滿足最小封裝原則,理論上只涉及一句SQL的執(zhí)行。如果有多個數(shù)據(jù)的存取動作,需要封裝在Service中,并用事務(wù)進行管理(雖然這么說,但repository在DDD中,是不和具體的數(shù)據(jù)庫打交道的)。
1.3 service和controller
這個沒什么好說的,基本上所有重要的邏輯都在這里完成。service用于邏輯處理,controller用于接口暴露。
2. 根據(jù)功能組織
大多數(shù)情況下,我們使用上面的這種劃分模式,能夠很好的完成工作。比如,所有的數(shù)據(jù)處理,都放在Dao層,所有的邏輯處理,都放在Service層。
這在小項目中相安無事,但如果項目中,有成百上千個Entity,這些目錄中的文件就會爆炸,以至于最后無法維護。
另外一個問題就是,僅僅一個簡單的功能,就可能分散在多個package下的多個文件中,大型項目維護變得困難。
我們有另外一個思路,就是根據(jù)功能進行分組。比如下面的截圖。
我們把相似功能,放在modules下的單個文件夾中。如果這個功能模塊比較大,我么可以在功能模塊下,再進行分層設(shè)計。
比如上圖,有一個商品服務(wù),我們單獨給它分配了一個目錄空間goods,然后在里面又劃分了dao、entity等目錄;但對于Service和Controller,我們簡單的放在了外層,可以看到在模塊內(nèi)的分配是比較靈活的。
這么做的好處是顯而易見的。功能變的非常的集中,各個package之間的內(nèi)容互不影響。
3. 還是不夠優(yōu)雅
其實,即使我們這樣劃分了,項目仍然會面臨很大的挑戰(zhàn)(很多DDD的書籍,會大量討論各層的交互)。
下面分享一個我在平常使用的分層模式,兼顧高內(nèi)聚和低耦合,有著良好的擴展性。
- config,最外層的一些全局配置,比如web配置,消息隊列配置等
- system,全局的工具和依賴功能,在DDD中叫做基礎(chǔ)設(shè)施(但在非DDD實踐的項目中名稱太怪異了)
- auth,權(quán)限認(rèn)證模塊,比如JWT或者Spring Sercurity,這部分的設(shè)計要獨立,以便后續(xù)抽離到Zuul之類的網(wǎng)關(guān)
- bc,在DDD中是限界上下文的意思(Bounded Context),我們也可以直接叫模塊,這些模塊有著嚴(yán)格的界限,可以根據(jù)請求量,拆分成相應(yīng)的微服務(wù)。在上圖,crm、images、order等等,都可以抽離成獨立的微服務(wù)
我們再來看一下每個模塊之內(nèi)的結(jié)構(gòu)。
- 和傳統(tǒng)的MVC類似。不過,為了屏蔽變化,兼顧擴展性,我們增加了更多的內(nèi)容。
- persitence,持久層,具體使用JPA還是Mybatis,這個是無關(guān)緊要的。我們的目標(biāo),就是盡量的弱化持久層的實現(xiàn),將變化封裝在Domain層中
- persitence/dao,具體的持久層接口,比如MyBatis的Mapper文件,或者JPA的Repository
- domain層,具體的業(yè)務(wù)層,你可以認(rèn)為是一堆Getter、Setter的Bean。我們盡量會把大多數(shù)驗證類和變化封裝在這里(可以大體認(rèn)為是DDD中的充血模型)
- controller,具體的Rest接口層。但不同的是,有很多不同的請求和返回,我們封裝成了Request和Response,用來接受提交的數(shù)據(jù),對返回數(shù)據(jù)進行瘦身等
- application,應(yīng)對傳統(tǒng)的service層,除了在application能夠調(diào)用Dao,其他層是沒有權(quán)利調(diào)用Dao的
- api,和application的功能是相同的。只不過,api的接口,指的是模塊之間可以相互調(diào)用的接口。除了api暴露的這些接口,bc之間的類和接口,默認(rèn)彼此是不可見的
- util,不通用的util,會放在模塊內(nèi)部,而不是抽離出公共的util
除了要解決目錄方面的問題,我們還要把數(shù)據(jù)的流向給規(guī)劃清楚。
一個上層的應(yīng)用,是可以通過API接口直接調(diào)用下層服務(wù)的。比如,訂單系統(tǒng)訪問商品基礎(chǔ)信息的數(shù)據(jù);反之卻不可以,比如商品基礎(chǔ)信息模塊訪問訂單系統(tǒng)的接口。
低層想要對高層的數(shù)據(jù)產(chǎn)生變化,就只能通過消息模塊,將變更發(fā)布出去,其他的模塊就可以訂閱這些變化。
小結(jié)
綜上所述,xjjdog認(rèn)為,如果你的項目,可能會比較大,單純的使用分層的package,并不是一個好的習(xí)慣。
你可能對這種后臺管理類的項目駕輕就熟,有很多有用的模版,它們都是簡單的MVC分層。這應(yīng)付一些外包項目,干一些一錘子買賣的時活,或許沒什么問題,但一旦是比較大的長期項目,這種分層的目錄接口就顯現(xiàn)出它的弊端。
這是因為:項目的短期風(fēng)險,是工期問題;而長期風(fēng)險,是擴展問題。隨著訪問量的增加,還有低耦合高內(nèi)聚的需求增加,如何快速的應(yīng)對需求,減少BUG,將會是制約項目發(fā)展的最主要因素。
作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎(chǔ)架構(gòu)和Linux。十年架構(gòu),日百億流量,與你探討高并發(fā)世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎添加好友,進一步交流。