目結(jié)構(gòu)如何設(shè)計(jì)才更清晰)
我見過太多SpringBoot項(xiàng)目啟動(dòng)類后面跟著五個(gè)包c(diǎn)ontroller、service、dao、entity、config。三個(gè)月后service包下膨脹出幾百個(gè)類命名從UserService到UserServiceImpl再到UserBizService沒人說得清某個(gè)功能到底在哪個(gè)類里。改一個(gè)需求要翻三四個(gè)包加一個(gè)字段要?jiǎng)游鍌€(gè)文件。這種結(jié)構(gòu)的核心問題不是“分不夠細(xì)”而是包結(jié)構(gòu)完全是技術(shù)分層的復(fù)讀從未表達(dá)過業(yè)務(wù)邊界。清晰的SpringBoot項(xiàng)目結(jié)構(gòu)第一原則是包結(jié)構(gòu)映射業(yè)務(wù)域而不是映射技術(shù)角色。技術(shù)分層是運(yùn)行時(shí)依賴的約定目錄是用來讓人快速理解“這個(gè)系統(tǒng)管哪些事”的。當(dāng)你打開項(xiàng)目的頂級(jí)包看到的應(yīng)當(dāng)是“訂單”“商品”“用戶”這樣的業(yè)務(wù)域而不是“controller”“service”“dao”這樣的技術(shù)層。業(yè)務(wù)域之間天然隔離改動(dòng)彼此獨(dú)立而技術(shù)層之間則存在強(qiáng)耦合任何需求都會(huì)穿透三層導(dǎo)致改動(dòng)無法局部化。有人會(huì)問那controller、service這些放哪里答案是放在業(yè)務(wù)域內(nèi)部作為二級(jí)包。比如com.company.order下面可以有controller、service、repository、domain但“tech.layer”作為頂級(jí)目錄只會(huì)讓代碼按照“我是什么”分類而不是按照“我屬于哪塊業(yè)務(wù)”分類。按業(yè)務(wù)域組織后你要改訂單相關(guān)功能直接進(jìn)入order域無需在全局幾十個(gè)技術(shù)包里做“語境切換”。這才是“清晰”的第一含義——減少導(dǎo)航成本。第二個(gè)必須想清楚的問題是項(xiàng)目結(jié)構(gòu)到底要為誰服務(wù)為一個(gè)剛啟動(dòng)的Demo服務(wù)單模塊加技術(shù)分層沒問題但一旦業(yè)務(wù)復(fù)雜到十幾個(gè)人協(xié)作結(jié)構(gòu)就必須能隔離團(tuán)隊(duì)、隔離發(fā)布、隔離失敗。很多人上來就拆多模塊結(jié)果拆出五個(gè)模塊互相同引最后干脆用一個(gè)公共common包放所有實(shí)體——這比單模塊更糟。拆分的核心依據(jù)是依賴方向而不是“看著整齊”。一個(gè)模塊如果被所有其他模塊依賴那它就是變相的全局垃圾場(chǎng)。SpringBoot項(xiàng)目里最典型的反面教材就是common或util模塊里面塞滿DateUtils、StringUtils、BaseEntity、Result最后沒人敢刪因?yàn)樗心K都引了它。第三個(gè)犀利觀點(diǎn)entity、DTO、VO的分離不是“多寫幾個(gè)類”的問題而是結(jié)構(gòu)腐化的分水嶺。很多項(xiàng)目為了“簡化”直接用entity充當(dāng)響應(yīng)體甚至把數(shù)據(jù)庫字段暴露給前端。等加了權(quán)限校驗(yàn)、脫敏、版本兼容需求時(shí)只能往entity里塞一堆JsonIgnore和臨時(shí)字段。正確的做法是讓每一層有自己的模型語言JPA實(shí)體只表達(dá)持久化結(jié)構(gòu)DTO負(fù)責(zé)API輸入輸出Domain對(duì)象承載業(yè)務(wù)規(guī)則。哪怕初期看起來重復(fù)但當(dāng)業(yè)務(wù)復(fù)雜到一定程度這種“重復(fù)”恰恰是保護(hù)層讓數(shù)據(jù)庫表結(jié)構(gòu)變化不至于炸到接口契約。那么包結(jié)構(gòu)究竟怎么落地我推薦“業(yè)務(wù)域內(nèi)部結(jié)構(gòu)”的清晰范式。頂級(jí)包按業(yè)務(wù)域拆分每個(gè)域內(nèi)部包含api對(duì)外接口與DTO、application應(yīng)用服務(wù)/用例、domain實(shí)體、值對(duì)象、領(lǐng)域服務(wù)、infrastructure持久化、外部客戶端。這個(gè)結(jié)構(gòu)借鑒了整潔架構(gòu)但不必拘泥于五層。關(guān)鍵是依賴方向必須從外向內(nèi)api依賴applicationapplication依賴domaininfrastructure依賴domain但被application反向依賴。SpringBoot的依賴注入讓這些方向天然正確但目錄卻常常寫反導(dǎo)致infrastructure里的工具類被controller直接調(diào)用繞過了業(yè)務(wù)規(guī)則。我見過最隱蔽的壞味道是“萬能service”。一個(gè)OrderService里同時(shí)處理支付回調(diào)、庫存扣減、積分發(fā)放、物流同步。表面看它“包治百病”實(shí)際上它違反了單一職責(zé)所有用例被揉進(jìn)一個(gè)上帝類。當(dāng)你想復(fù)用“確認(rèn)訂單”邏輯時(shí)發(fā)現(xiàn)它必須在某個(gè)service方法里而且你沒法只調(diào)用它而避開其他副作用。破局之道是把“用例”作為一等公民——一個(gè)用例一個(gè)類命名如PlaceOrderUseCase、CancelOrderUseCase。這些類放在application包下它們編排domain對(duì)象和repository接口而不再有所謂的“業(yè)務(wù)service”層。這會(huì)讓項(xiàng)目結(jié)構(gòu)看起來類變多了但每一個(gè)類的名字都直指業(yè)務(wù)意圖可測(cè)試性也大幅提升。另外包與包之間的循環(huán)依賴是結(jié)構(gòu)清晰度的最大殺手。在SpringBoot中循環(huán)依賴有時(shí)能被容器“勉強(qiáng)”解決但包結(jié)構(gòu)的循環(huán)依賴會(huì)讓人徹底崩潰。比如order包的service反向依賴了user包的repository而后者的某處又依賴了order的DTO。這種糾纏一旦出現(xiàn)你無法獨(dú)立改動(dòng)任何一個(gè)域任何局部重構(gòu)都像在雷區(qū)跳舞。強(qiáng)制規(guī)則很簡單業(yè)務(wù)域名之間禁止相互依賴跨域協(xié)作通過領(lǐng)域事件或解耦接口進(jìn)行。具體做法是當(dāng)訂單需要用戶信息時(shí)不要在order包里直接注入U(xiǎn)serRepository而是定義UserQueryPort接口讓infrastructure層實(shí)現(xiàn)這個(gè)端口。這樣order包只依賴一個(gè)抽象契約真正的用戶狀態(tài)由user域負(fù)責(zé)。還有一個(gè)高頻痛點(diǎn)Controller里堆業(yè)務(wù)邏輯。有人把參數(shù)校驗(yàn)、數(shù)據(jù)組裝、權(quán)限判斷全寫在Controller方法里一個(gè)方法上百行。這和包結(jié)構(gòu)無關(guān)卻直接反映結(jié)構(gòu)設(shè)計(jì)缺失。Controller的唯一職責(zé)是解析HTTP、綁定參數(shù)、調(diào)用應(yīng)用服務(wù)、返回響應(yīng)。任何超出這些的動(dòng)作都必須下沉到application或domain。否則你會(huì)看到Controller依賴了repository、甚至事務(wù)注解——這意味著技術(shù)層之間的邊界已經(jīng)蕩然無存。判斷結(jié)構(gòu)是否清晰最簡單的方法是看一個(gè)改動(dòng)需要同時(shí)觸碰幾個(gè)包。讓我用一次真實(shí)重構(gòu)展示效果。原項(xiàng)目頂級(jí)包是controller/service/mapper/entity其中service有120個(gè)類實(shí)體和VO混用。重構(gòu)后頂級(jí)包變?yōu)閛rder/product/user/payment/customer-support每個(gè)業(yè)務(wù)域下用api/application/domain/infrastructure組織。原UserService被拆成GetUserUseCase、UpdateProfileUseCase等并把UserEntity拆成UserAccountDO持久化和UserProfileVO響應(yīng)。結(jié)果修改用戶頭像的功能只動(dòng)了user里的四個(gè)文件不需要打開order包新接一個(gè)支付渠道時(shí)新增類只在payment/infrastructure里。整個(gè)系統(tǒng)的可理解性直接從“按打字員分類”升級(jí)為“按業(yè)務(wù)負(fù)責(zé)人分類”。有人擔(dān)心這種結(jié)構(gòu)過度設(shè)計(jì)對(duì)于小項(xiàng)目是負(fù)擔(dān)。確實(shí)一個(gè)只有三個(gè)實(shí)體的CRUD服務(wù)按業(yè)務(wù)域加用例類會(huì)顯得笨重。但結(jié)構(gòu)設(shè)計(jì)從來不是為今天寫的而是為三個(gè)月后的預(yù)期復(fù)雜度寫的。更務(wù)實(shí)的做法是漸進(jìn)式重構(gòu)先保證頂級(jí)包按業(yè)務(wù)域劃分哪怕內(nèi)部暫時(shí)保留controller/service/dao當(dāng)某個(gè)業(yè)務(wù)域里的service超過十個(gè)方法或三個(gè)用例時(shí)再內(nèi)部引入application和domain。 記住清晰的結(jié)構(gòu)不是一次畫出來的是隨著對(duì)業(yè)務(wù)理解的加深不斷演進(jìn)出來的。關(guān)鍵是建立一套“依賴方向”和“命名即意圖”的紀(jì)律并把這種紀(jì)律寫進(jìn)CheckStyle或ArchUnit測(cè)試?yán)镒孋I攔住下一次腐化。如果給所有原則排個(gè)序我最看重一條包結(jié)構(gòu)應(yīng)當(dāng)是“按業(yè)務(wù)能力垂直切片”而不是“按技術(shù)細(xì)節(jié)水平分層”。垂直切片意味著每個(gè)業(yè)務(wù)域擁有自己從API到持久化的完整實(shí)現(xiàn)改動(dòng)被限制在切片內(nèi)水平分層則讓每次需求穿過所有層等于每次改動(dòng)都在“全量編譯”。SpringBoot給了你極大的自由但也給了你極大的腐蝕空間。真正的清晰不是目錄多么優(yōu)雅而是任何一位新成員都能在十分鐘內(nèi)找到“某個(gè)操作”應(yīng)該住的包并且改完不擔(dān)心影響別處。當(dāng)你的項(xiàng)目做到這一點(diǎn)SpringBoot結(jié)構(gòu)設(shè)計(jì)才算真正及格。