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