者的模塊化設計思路與實例)
模塊化這個被Java開發(fā)者念叨了二十年的詞今天比任何時候都更需要被重新審視。很多人以為把類分到幾個包、把項目拆成幾個Maven模塊就叫模塊化。但當你真的面對一個超過五十萬行代碼、十幾個團隊共同維護的系統(tǒng)時你會發(fā)現(xiàn)包和模塊之間的邊界往往模糊得像晨霧里的海岸線。模塊化的本質(zhì)不是物理隔離而是心智隔離——讓一個開發(fā)者能在不看其他模塊源碼的情況下理解并修改自己負責的那部分。你需要的不是拆分的技巧而是識別“什么該被邊界保護”的能力。模塊化的底層語言依賴方向才是主角Java里的package是信息隱藏的最小單位但package無法限制依賴方向。一個團隊可以輕易地讓自己的類去import另一個團隊的內(nèi)部實現(xiàn)類沒人能攔得住。這就是為什么Java 9推出JPMS時把module-info.java稱為“軟件設計的一等公民”。我們來做一個真實場景假設你有一個訂單系統(tǒng)內(nèi)部有order-api、order-core、order-infrastructure三個包。傳統(tǒng)Maven項目里order-infrastructure中的MyBatisUserRepositoryImpl可以被任何類import哪怕某個Controller直接調(diào)用它也能編譯通過。而使用JPMS你可以在order.core的module-info.java里寫上module order.core { exports com.example.order.api; requires java.sql; uses com.example.order.spi.UserRepository; }然后讓order.infrastructure提供實現(xiàn)但不導出任何包給其他模塊。此時其他模塊想用數(shù)據(jù)訪問層對不起編譯直接報錯。這就是模塊化設計思路的核心依賴方向必須和業(yè)務意圖一致否則代碼就是一團亂麻。不要被“模塊”這個詞騙了邊界是約束不是功能很多開發(fā)者喜歡把所有類都設成public然后借口“方便測試”?!胺凑际峭粋€項目寫在一起省事”是個極其危險的思維。真正可維護的模塊系統(tǒng)對外暴露的接口數(shù)量應該遠遠小于內(nèi)部實現(xiàn)類的數(shù)量。我見過一個系統(tǒng)某個模塊的public類有237個但真正被其他模塊調(diào)用的只有11個。另外226個public類要么是內(nèi)部實現(xiàn)細節(jié)要么是被反射調(diào)用。這種設計下模塊邊界形同虛設。JPMS給了你一個強制手段module-info.java里exports什么才是什么。你可以在模塊里用package-private定義所有內(nèi)部類只在API包中放置公開接口。接口才是模塊的門面門面越窄系統(tǒng)越穩(wěn)。我們來看一個具體的實例。假設你要設計一個支付模塊支持支付寶、微信和銀行卡。錯誤的做法是// 錯誤把三個支付實現(xiàn)類全部設為public暴露給所有調(diào)用方 public class AlipayClient { ... } public class WechatClient { ... } public class CardClient { ... }正確的模塊化思路是只暴露PaymentService接口和PaymentRequest/PaymentResult兩種數(shù)據(jù)結構。三個支付客戶端放在payment.impl包中使用package-private修飾只在模塊內(nèi)部的PaymentServiceFactory中組裝。調(diào)用方只依賴PaymentService。這樣以后新增“銀聯(lián)支付”你在模塊內(nèi)部加一個類外部代碼一行都不用改。這就是Open-Closed Principle在模塊層的落地對擴展開放對修改封閉。實例從“工具類粘貼”到“真正模塊化”的重構如果你還覺得抽象我講一個真實的尷尬案例。某金融項目里有個DateUtil類因為太“實用”被25個模塊的幾百個類直接調(diào)用。諷刺的是這個DateUtil內(nèi)部緩存了一個SimpleDateFormat而它是線程不安全的。每年總有那么幾天交易系統(tǒng)會出現(xiàn)奇怪的時間偏移錯誤。問題根因就是沒有模塊邊界工具類變成了一個失控的公共廣場。重構時我們做了三件事第一把DateUtil拆成DateTimeProvider接口和SystemDateTimeProvider實現(xiàn)。第二用JPMS強制要求需要時間的模塊只能requires一個time.api模塊且只拿到接口。第三原來直接靜態(tài)調(diào)用的地方全部改成依賴注入。這個過程不復雜但觸動了很多人的“舒適區(qū)”。有人問“直接調(diào)static方法多簡單非要繞一圈?!蹦阌X得繞圈是因為你還沒吃過沒有邊界時那種“牽一發(fā)動全身”的苦。模塊化的另一個戰(zhàn)場類加載器與運行時的隔離模塊化設計不只在編譯期還涉及運行時。OSGi之所以在Java 9之前被追捧根本原因是它提供了動態(tài)的模塊生命周期服務可以在運行時安裝、卸載、更新而不需要重啟JVM。JPMS相比之下是靜態(tài)的——模塊在啟動時確定無法動態(tài)添加。但JPMS解決了一個更底層的問題強封裝的可靠性。注意Java 9之前的包名隔離是“約定”Java 9之后的模塊隔離是“法律”。我們做一個對比在OSGi里你可以用Import-Package和Export-Package精確控制類可見性在JPMS里你用requires和exports。兩者思路一致但語法和生態(tài)完全不同。一個有趣的點是模塊化的目的是讓每個模塊可以獨立演化和替換。如果你的模塊之間通過線程、全局靜態(tài)變量、ThreadLocal、文件系統(tǒng)共享狀態(tài)那么模塊化設計就是空中樓閣。依賴注入容器如Spring的興起某種程度上就是為了在模塊之間傳遞依賴而不直接引用具體類。但Spring本身并不能強制模塊邊界——你依然可以在Autowired字段上寫一個內(nèi)部類。真正決定邊界的是你在寫代碼時內(nèi)心的那一把尺子。每個import語句都是你在做一次架構決策只是大多數(shù)人都沒意識到。流水線式模塊鏈一個完整的支付系統(tǒng)設計現(xiàn)在讓我把多個模塊組織起來。假設我們要做一個支付系統(tǒng)目標是支持多種支付渠道且未來接入新渠道不需要改動核心流程。我們設計四個模塊payment-api定義PaymentService、PaymentRequest、PaymentResult。payment-core包含支付流程編排比如提交訂單、調(diào)用渠道、記錄日志處理回調(diào)。它只依賴payment-api。payment-alipay、payment-wechat分別實現(xiàn)payment-api中的PaymentChannel接口。這些模塊在編譯期requires payment.api運行時通過ServiceLoader或Spring注入到payment-core。這里的關鍵是——payment-core永遠不直接出現(xiàn)“Alipay”或“Wechat”的字樣。它只面向PaymentChannel接口。測試時你把一個Mock的PaymentChannel塞進去就跑完了全流程。生產(chǎn)時你把支付寶實現(xiàn)module放到classpath它就生效。這是模塊化系統(tǒng)最誘人的特性可插拔性。這種設計模式在很多框架里叫Strategy模式但模塊化把它從“類和接口的層級”提升到了“組件和依賴的層級”。你不再需要修改PaymentProcessor來添加渠道只需要新增一個jar放到部署目錄配置一行路由。說說模塊化的反模式你很有可能正在犯第一個反模式濫用模塊依賴形成循環(huán)依賴。比如module-a依賴module-b而module-b又需要module-a里的某個類。在Maven多模塊工程里這會導致編譯失敗在JPMS里這是直接禁止的。循環(huán)依賴在業(yè)務上看似合理本質(zhì)上說明兩個模塊的邊界劃錯了——正確的做法是把共同依賴的部分抽出去形成第三層。比如A需要B的訂單查詢B需要A的庫存預占那應該把OrderService和InventoryService都定義在trade-api模塊中讓他們分別實現(xiàn)而不是互相依賴。第二個反模式模塊粒度過小。有人把10000行代碼拆成100個模塊每個模塊只有兩個類。這是把“模塊”當成了“類的分組”純粹為了拆而拆。模塊的價值是便于獨立部署或獨立維護如果拆完的每個模塊都需要同時發(fā)布、同步版本那和沒拆沒有任何區(qū)別。好的模塊粒度應該以“業(yè)務能力”為邊界而不是按“技術分層”來切。比如“用戶”“訂單”“支付”是好的模塊粒度而“controller”“service”“dao”這樣的分層是極差的模塊化方式——因為一個功能用例必然橫跨這三個層拆完等于沒拆。第三反模式模塊依賴了具體的數(shù)據(jù)庫或中間件。比如payment-core直接requires java.sql并寫了查詢語句那就意味著該模塊無法脫離MySQL運行。模塊化的高階目標之一是“可替換基礎設施”。如果你的模塊內(nèi)部硬編碼了Redis鍵、Kafka topic名、文件路徑那你只是把一堆代碼勉強塞進了模塊系統(tǒng)并沒有獲得模塊化的核心收益。這些外部資源應該通過uses和provides機制抽象出來讓真正的基礎設施模塊在運行時綁定。與微服務的關系模塊化是微服務的基礎有人會說既然微服務已經(jīng)通過進程邊界隔離服務了我們還需要模塊化嗎答案是更需要的。微服務的第一原則是“服務內(nèi)高內(nèi)聚服務間低耦合”。如果你把整個項目寫成一個巨大的Spring Boot應用然后用一些粗糙的Service類來組織再硬拆成十幾個微服務你會痛苦地發(fā)現(xiàn)服務拆了但模塊邊界沒拆結果每個微服務里都塞進了一堆不屬于自己的代碼。真正的做法是先在單體應用內(nèi)部用JPMS或OSGi把模塊邊界畫清楚然后再把某些邊界提升為進程邊界。模塊化設計是一種“演進式架構”的基石。你可以先把模塊放在同一個JVM里通過接口交互確認依賴關系穩(wěn)定了再把某個模塊抽成獨立服務。如果一開始就不做模塊化直接上微服務那你只是在物理上隔離了代碼但是邏輯上依然是一團耦合的泥球。著名微服務架構師Sam Newman有一句話說得很犀利“微服務的核心不是微而是服務邊界?!倍者吔绲亩x能力正是模塊化設計訓練出來的。實用技巧用ArchUnit和jdeps守護模塊化就算你的模塊劃分再科學任由團隊自由發(fā)展半年后也會腐爛。所以需要工具來強制約束。ArchUnit是一個不錯的JUnit測試庫它可以檢查包依賴方向比如不允許infrastructure包被api包依賴。禁止payment-core模塊中的類直接import任何payment-alipay中的類。強制controller包只能調(diào)用service包不能直接觸碰mapper包。你把這些規(guī)則寫成單元測試在CI里每次跑。這是模塊化設計的“交通法規(guī)”——別指望司機自覺必須裝攝像頭和罰單。另一個工具是JDK自帶的jdeps命令。它可以分析你JAR文件的模塊依賴輸出module-info建議甚至檢測出“隱式依賴”或“非法反射訪問”。比如你寫了個module customer { requires java.base; }但代碼里偷偷用了sun.misc.Unsafejdeps會警告。模塊化不是一次性設計而是持續(xù)的設計紀律。從思想到實踐一份模塊化設計檢查清單與其背誦理論不如用下面這些準則來復盤你的項目。第一條問自己這個模塊被其他人改的時候會不會無意間破壞別的模塊如果會說明邊界還不夠硬。第二條看導出面的數(shù)量如果每個模塊都導出超過20個公共類型你大概率沒有設計好。試著把內(nèi)部實現(xiàn)類降為包級私有只留一組門面接口。第三條測試依賴是否合理如果測試類必須import其他模塊的internal包說明測試邊界被打破你需要在模塊內(nèi)提供測試入口。第四條觀察版本演進如果每次修改一個業(yè)務功能需要同時改動三個以上模塊那么模塊切分和業(yè)務邊界不一致。模塊化設計真正考驗的是你對“不確定性的管理能力”。未來的需求會改變技術棧會升級團隊會重組。你通過模塊化來隔離這些變化讓自己在變化發(fā)生時只需要動一個模塊而不是整個系統(tǒng)。Java從誕生到JPMS的完整實現(xiàn)走了二十多年說明這件事不簡單。但正因為不簡單才能拉開“合格程序員”和“優(yōu)秀架構師”之間的差距。當你開始主動思考“模塊的職責邊界在哪里”而不是“我把這個類放哪個包”你已經(jīng)跨過了那道最重要的門檻。最終Java開發(fā)者的模塊化設計思路不是一套規(guī)則而是一種對“復雜度”的敬畏。每一個不加約束的依賴都在為未來的崩潰埋下伏筆。而你每一次劃定邊界、收斂出口、控制依賴方向都是在把不可預測的失控進程拽回理性設計的軌道。你不需要一步到位但可以從一個模塊開始從上圖那個module-info.java開始。所有偉大的架構都是從第一道清晰的邊界開始的。