一種流行的方法是經過技術層面對項目進行分包。可是這種方法有一些缺點。相反,咱們能夠按功能分包並建立獨立自治的程序包。結果是一個易於理解且不易出錯的代碼庫。java
按照技術分包形成的缺點:數據庫
對屬於某個功能的全部類的概述不佳。api
通用代碼、重用代碼和複雜代碼趨向於難以理解,而且因爲難以把握變動的影響,所以變動很容易破壞其餘功能用例。架構
按功能分包從而建立包含功能所需的全部類的程序包。好處是:dom
項目結構的一種很是流行的方法是逐層分包。這將爲每一個技術組所屬類提供一個軟件包。測試
⚠️:按層分包從技術角度對全部類進行分組編碼
讓咱們將調用層次結構添加到圖片中,以「清楚地」瞭解哪一個類取決於其餘哪一個類。線程
⚠️:調用層次結構遍佈整個項目,涉及許多包翻譯
那麼,按層分包的缺點是什麼?日誌
功能概述不佳。一般,當咱們在項目中處理代碼時,咱們首先會想到要更改的特定領域或功能。所以,咱們會從領域的角度出發。不幸的是,按技術分層分包迫使咱們從一種軟件包過渡到另外一種軟件包,才能掌握功能的概況。
通用,重用和複雜代碼的趨勢。一般,這種方法致使中心類包含每一個功能用例的全部方法。隨着時間的流逝,這些方法愈來愈抽象化(帶有額外的參數和泛型)來知足更多用例。上圖中僅一個示例是ProductDAO,其中放置了ProductController和ExportController的方法。結果是:
當添加更多方法時,類將變得更大。所以,僅憑代碼量,就很難理解它。
更改通用重用代碼很危險。儘管您只想處理一個用例,但您能夠輕鬆地打破全部用例。
因爲如下兩個緣由,難以理解抽象方法和通用方法:首先,要通用,一般須要其餘技術構造(例如,switch,參數,泛型),這使得查看與當前用例相關的業務邏輯更加困難。其次,認知需求更高,由於您必須瞭解全部其餘用例,以確保您不會破壞它們。
桑迪·梅斯(Sandi Metz)指出:
「我以爲我必須瞭解全部內容才能提供幫助。」桑迪·梅斯(Sandi Metz)。請參閱個人帖子,瞭解咱們的編碼智慧牆。
⚠️:咱們達到了DRY,但違反了KISS。
讓咱們將這些類從新排列成獨立的功能包。
👆用戶管理功能包
新的包userManagement包含屬於此功能的全部類:控制器,DAO,DTO和實體。
👆產品管理功能包
新軟件包productManagement包含相同的類類型,以及StockServiceClient和相應的StockDTO。這個事實清楚地代表:庫存服務僅由產品管理人員使用。
userManagement和productManagement使用不一樣的域實體和表。將它們分紅不一樣的包很簡單。可是,當一個功能須要與另外一個功能類似或甚至相同的域實體時,會發生什麼?
👆產品出口的功能包
如今,它變得愈來愈有趣。exportProduct包也處理產品實體,但具備不一樣的功能用例。
咱們的目標是擁有獨立自治的功能包。所以,exportProduct應該具備本身的DAO,DTO類和實體類,即便它們看起來與productManagement中的類類似。抵制重用productManagement中的類的衝動。
咱們可能不得再也不次編寫更多代碼,但最終會遇到很是有利的狀況:
上面的功能包很棒,但實際上,咱們將始終須要一個通用的包。
👆通用軟件包包含技術配置和可重複使用的代碼
它包含技術配置類(例如用於DI,Spring,對象映射,http客戶端,數據庫鏈接,鏈接池,日誌記錄,線程池)
它包含可重用的有用代碼片斷。可是要很是當心代碼的過早抽象。我老是先把代碼放到儘量接近它的用法的地方,也就是特性包,甚至是使用類。僅當片斷確實有更多用途(⚠️:而不是我認爲未來可能會使用)時,纔將其移動到通用包中。三定律提供了很好的指導。
在通用包中找到全部實體多是有意義的。咱們還對某些項目執行了此操做,其中許多功能包一次又一次地使用相同的實體。一些開發人員還但願將全部實體放在中心位置,以便可以總體查看數據庫架構的映射。目前,我並非教條,由於實體的兩個位置均可以合理。不過,一開始我老是儘量多地將代碼轉移到功能包中,並依賴於定製的特定於用例的實體和投影。
最終,咱們的大圖看起來像這樣:
👆按功能分包的大圖
讓咱們簡要總結一下好處:
可是,我認爲優勢大於缺點。
擬議的按功能分包方法遵循的原則很是貼切:
KISS > DRY
再次,我想引用桑迪·梅斯(Sandi Metz)
「我以爲我必須瞭解全部內容才能提供幫助。」桑迪·梅斯(Sandi Metz)。請參閱個人帖子,瞭解咱們的編碼智慧牆。
咱們的團隊記錄了其遵循的編碼準則和原則。關於按功能分包的部分以下所示:
咱們基於功能分包。每一個功能包均包含提供該功能所需的大多數代碼。每一個功能包都應獨立且自治。
├── feature1 │ ├── Feature1Controller │ ├── Feature1DAO │ ├── Feature1Client │ ├── Feature1DTOs.kt │ ├── Feature1Entities.kt │ └── Feature1Configuration ├── feature2 ├── feature3 └── common
這取決於項目和功能包的大小。
對於中小型項目,我喜歡避免定義可能會增長更多儀式而非價值的規則(例如,要求定義某些接口和子包)。只要您構建獨立的、自治的、從您的特定業務領域派生的包,您就在正確的軌道上。
若是要處理更大的代碼庫,則可能須要定義有關子包結構和方式的更多規則,則容許一個功能包訪問另外一個功能包。「模塊」或「組件」而不是「功能包」的概念可能更有幫助。例如,Tom Hombergs建議在每一個組件包中添加api和內部包,這些組件包定義組件的哪些部分容許其餘組件使用。有關詳細信息,請參閱他的文章「使用Spring Boot和ArchUnit清理架構邊界」。
是的,會有一些重複,可是根據個人經驗,您可能不會相信那麼多100%相同的代碼。因爲類似的代碼涵蓋了不一樣的用例,所以一般是不一樣的。例如,兩種方法能夠按產品名稱查詢產品,可是它們在計劃的字段,排序和其餘條件方面有所不一樣。所以,最好將方法分開放在不一樣的程序包中。
並且,複製自己並非邪惡的。在開始將代碼提取到通用重用方法以前,我喜歡應用三定律。
最後,我想強調指出,仍然容許集中使用可重用的代碼,有時甚至是合理的,可是這些狀況再也不那麼常見了。
分包方法與語言無關。可是Kotlin使其易於遵循:
使用數據類,編寫量身定製的特定於功能的結構(如DTO或實體)僅需幾行,而無需樣板。
Kotlin容許將多個類放在一個文件中。所以,咱們可使一個包含全部數據類定義的DTOs.kt或Entities.kt文件成爲一個單獨的DTOs.kt或Entities.kt文件,而不是有一個子包DTO或包含每一個POJO類的許多Java文件的實體。
本文翻譯自:https://phauer.com/2020/package-by-feature/
關注筆者公衆號,推送各種原創/優質技術文章 ⬇️