Skip to main content

Ch34: 遺漏的章節

方法一:依套件逐層打包 (Package by layer)

水平橫向分層架構 (horizontal layered architecture)

  • 最簡單的傳統設計方法,根據技術角度將程式碼分開,相同類型的在同一層,每一層只對相鄰的下層有依賴關係

圖片34.1 逐層打包

圖片34.1 逐層打包

說明

  • OrdersController : 一個 Web 控制器,類似 Spring MVC 控制器,用來處理來自 Web 的請求
  • OrdersService : 定義與訂單有關的 「業務邏輯」的介面
  • OrdersServiceImpl : OdersService 的實作
  • JdbcOrdersRepository : 儲存庫介面的實作

Example

├── com.app
└── controller
├── CompanyController
├── ProductController
└── UserController
└── model
├── Company
├── Product
└── User
└── repository
├── CompanyRepository
├── ProductRepository
└── UserRepository
└── service
├── CompanyService
├── ProductService
└── UserService
└── util

(出處: Package by Layer vs Package by Feature)

  • 採用這種分層架構做為開始是個好方式
  • 適合用於需要快速建立及沒有大量複雜性的情況下使用
  • 軟體規模擴大及複雜度增加後,需要進一步的模組化

方法二:依功能特性打包(Package by feature)

垂直切片 (vertical slicing)

  • 基於相關特性 (feature)、領域概念或聚合根 (aggregate roots: 取自領域驅動設計 DDD 的術語) 而作劃分
  • 所有型態 (types) 都放在單一個 Java 包 (package: 套件) 中,其命名即反應出分組的概念

圖片34.2 依功能特性打包

圖片34.2 依功能特性打包

說明

  • 有上一個方法 (Package by layers) 相同的介面和類別,但全部放在單一個 Java package 中
  • 程式碼最高層級組織現在 「尖叫」出一些關於業務領域的內容
  • 當 Use case 變更時,較易找出所有需要修改的程式碼

Example

├── com.app
└── company
├── Company
├── CompanyController
├── CompanyRepository
└── CompanyService
└── product
├── Product
├── ProductController
├── ProductRepository
└── ProductService
└── util
└── user
├── User
├── UserController
├── UserRepository
└── UserService

(出處: Package by Layer vs Package by Feature)

小結

不論 Package by layer 或 Package by feature,兩者都不是最理想的選擇


方法三:端口和轉接器(Port and Adapter)

以業務/領域為中心的程式碼

  • 架構又稱為 Hexagonal Architecture(六角形架構),Clean Architecture 的核心 (參考:Clean Architecture(2):Port and Adapter Architecture
  • 獨立且與技術實作細節(如框架和資料庫)分離
  • 代碼庫(codebase)由「內部」(領域 Domain)和「外部」(基礎設施 infrastructure)所組成

圖片34.3 具有內部和外部的程式碼庫

圖片34.3 具有內部和外部的程式碼庫

說明

  • 外部依賴內部

    • 內部區域包含 domian 概念
    • 外部則包含與外部世界 (UI、DB、3rd-party library) 的互動

圖片34.4 view orders 使用案例

圖片34.4 view orders 使用案例

說明

  • 「內部」所有的內容命名該使用「普及的領域語言」(DDD 中的建議)
    • OrdersRepository 重新命名為 orders

方法四:依元件打包(Package by component)

基於元件的架構 (CBA - Component Based Architecture)

  • 一種混合式方法,目標將『與單個粗粒度 (Coarse-grained) 有關的所有責任』綁定(Bunddle) 到單一的 Package 中
  • 以服務 (Service) 為中心的軟體系統觀點
  • 元件:「業務邏輯」和持久性程式碼 bunddle 成單一的東西

Uncle Bob:
元件(Component)是部署(Development)的單位,它們是系統在部署時最小的部分實體。

(像是 Java 中的 jar 檔;使用 React 作為前端開發)

圖片34.5 寬鬆的分層架構

圖片34.5 寬鬆的分層架構

說明

  • 為了讓依賴箭頭指向下,OrderService 介面要標為 public,同樣 OrdersRepository 介面也須要標為 public
  • 這時有位新人加入團隊,要實作一個新的 Order 相關的使用案例,他可能會寫出如上的架構

圖片34.6 view orders 使用案例

圖片34.6 view orders 使用案例

說明

  • 將業務邏輯以及資料存取打包在一起,視為一個 元件 (component)
    • 兩者持久性是分離的,例: 一個單獨的 OrderService 封裝「與處理訂單有關的所有內容」
  • 元件是部署 (deploymen) 的單位
    • Web 相關的 controller 依賴 OrdersComponent
    • 在元件內部,關注點的分離依然不變

補充:C4 Model

  • Context (System Context Diagram)
    • 將各種使用者/角色需要使用的功能畫出來,不會有技術名詞或實作的相關做法
  • Containers
    • applications and data stores
  • Components
    • 將應用程式內容更深入地畫出每個元件
  • Code
    • 提供架構元素的設計細節,可以對應到程式碼

圖片 - C4 model

圖片 C4 Model (延伸閱讀:軟體架構之C4模型)

圖片 - C4 model

圖片 C4 Model


討論

這屬於哪一種架構?

└── foresight-ats.app
└── constants
└── src
├── assets
| └── img
└── lib
└── api
└── constainers
└── machines
└── mock
└── ui
└── src
├── components
└── lib

魔鬼就在實作細節中

組織與封裝

勿濫用 "Public"

  • 若將所有型態都宣告為 public,那麼 package 只是一種組織機制(用來分組,就像資料夾那樣),而不是用於封裝
  • 若都使用 public 型態,最終這些 package 容易被忽略,前面所提的四種架構方法都相同了

圖片34.7 四種架構方法都是相同的

圖片34.7 四種架構方法都是相同的

說明

  • 不論使用哪種架構方法都是相同的
  • 當所有型態都宣告 public 時,等於只是用四種方式來描述傳統的水平分層架構

圖片34.8 變成灰色的型態是可以使存取修飾符更具限制的地方

圖片34.8 變成灰色的型態是可以使存取修飾符更具限制的地方

說明

  • 適當使用存取修飾符,可影響這些型態的可存取性(或不可存取性)
  • 逐層打包
    • OrdersService 和 OrdersRepository 因為定義了被使用的介面 (依賴關係),被宣告為 public
    • OrdersServiceImpl 和 JdbcOrdersRepository 因為是實作細節,不具有對外性
  • 依功能特性打包
    • 只有 OrdersController 提供唯一的入口,其餘都不具有對外性
    • 除非透過這個 package 的 controller,否則其他程式碼沒有存取跟 orders 有關的資訊
  • 依端口與轉接器打包
    • OrdersService 和 Orders 因為屬於內部,需要被外部依賴,因此要為 public
    • 其餘 OrdersServiceImpl 和 JdbcOrdersRepository 屬於實作細節,在 runtime 時進行 dependency injection
  • 依元件打包
    • OrdersComponent 為 Controller 要互動的介面,須為 public,其餘皆視為實作細節

其他的解耦模式

通常還有其他方式可以將原始碼的依賴關係解耦

  • 使用 OSGi 和新的 Java 9 模組系統(module system)
  • 使用模組系統時,可區分 public 型態和 published (發布的) 型態
    • 建立一個 Orders 模組,所有型態都標記為 public,但僅將其中一個子集 publish 給外部使用

透過在不同的原始碼樹(different source code trees)支間分割程式碼

  • 以「端口和轉接器」為例,可有三棵原始碼樹

    • 業務和領域的原始碼(即獨立於技術和框架選擇的所有內容): OrdersService,OrdersServiceImpl 和 Orders
    • Web 的原始程式碼:OrdersController
    • 資料持久性的原始程式碼:JdbcOrdersRepository

圖片34.9 領域和基礎設施的程式碼

圖片34.9 領域和基礎設施的程式碼

說明

  • 有些會採用更簡單的方式,只區分兩個 source code tree,如上圖

    • Domain (內部)
    • Infrastructure (外部)
  • 將所有的 infra 實作都放在同一個 source code tree 當中,可能會出現一種 anti-pattern,程式中某個 infra (ex: Web controller) 可能會直接呼叫其他的 infra (ex: DB),而無須透過中間的 domain


總結

  • 考慮團隊的規模、技術水平及解決方案的複雜性和時間及預算的限制
  • 如何利用編譯器來幫助強制執行所選擇的架構風格
  • 注意其他區域(如資料模型)中的耦合