字數總計:0 個 | 閱讀時長:0 分鐘 |閱讀次數:

BOUNDARY ANATOMY (邊界剖析)

一個系統的架構由一系列軟體元件及它們之間的邊界共同定義,這些邊界有著多種不同存在形式。 在本章可以看看最常見的一些形式

BOUNDARY CROSSING 跨邊界調用

跨邊界調用是軟體架構中的一個關鍵概念。 在運行時,它實際上是邊界一側的函數調用另一側的函數,並在過程中傳遞數據。

這種調用看似簡單,但其背後涉及到複雜的程式碼依賴管理。

構建合適的跨邊界調用機制,核心在於如何妥善處理這些依賴關係。 這不僅關係到程序的正常運行,更影響到整個系統的可維護性和擴展性。

  • 來看個例子 訂單與庫存管理
    • 尚未明確的邊界時
      // 庫存管理類
      class InventoryManager {
          checkStock(productId: string): boolean {
              console.log(`檢查產品 ${productId} 的庫存`);
              return true; // 假設總是有庫存
          }
      }
      
      // 訂單處理類
      class OrderProcessor {
          private inventoryManager: InventoryManager;
      
          constructor() {
              this.inventoryManager = new InventoryManager();
          }
      
          processOrder(productId: string): void {
              if (this.inventoryManager.checkStock(productId)) {
                  console.log(`處理產品 ${productId} 的訂單`);
              } else {
                  console.log(`產品 ${productId} 缺貨`);
              }
          }
      }
      
      // 使用示例
      const orderProcessor = new OrderProcessor();
      orderProcessor.processOrder("特大杯冰拿");
      
    • 引入邊界後的設計
      // 庫存檢查介面
      interface StockChecker {
          checkStock(productId: string): boolean;
      }
      
      // 庫存管理類實現 StockChecker 介面
      class InventoryManager implements StockChecker {
          checkStock(productId: string): boolean {
              console.log(`檢查產品 ${productId} 的庫存`);
              return true; // 假設總是有庫存
          }
      }
      
      // 訂單處理類
      class OrderProcessor {
          constructor(private stockChecker: StockChecker) {}
      
          processOrder(productId: string): void {
              if (this.stockChecker.checkStock(productId)) {
                  console.log(`處理產品 ${productId} 的訂單`);
              } else {
                  console.log(`產品 ${productId} 缺貨`);
              }
          }
      }
      
      // 使用示例
      const inventoryManager = new InventoryManager();
      const orderProcessor = new OrderProcessor(inventoryManager);
      orderProcessor.processOrder("特大杯冰拿");
      

那麼,為什麼程式碼依賴管理如此重要呢?這是因為軟體開發是一個持續變化的過程。

當一個程式碼模組發生變更時,它可能會觸發一連串的反應:其他相關模組可能需要修改,重新編譯,甚至重新部署。

這種連鎖反應可能會導致系統不穩定,增加維護成本。

因此,邊界的主要作用就是在這些模塊之間建立"防火牆",以管理和控制這種變更的傳播。

通過合理設置邊界,我們可以將變更的影響限制在特定範圍內,從而提高系統的穩定性和可維護性。

THE DREADED MONOLITH 令人生畏的單體結構

單體架構是最常見的系統架構形式,它將所有功能和數據劃分在同一個進程和地址空間內。儘管這些邊界在部署時不可見,但對於獨立開發系統組件非常重要。

單體架構依賴動態多態來管理內部依賴關係,低層客戶端通常直接調用高層服務函數,而當高層需要低層服務時,則利用動態多態反轉依賴。

在單體架構中,組件間的通信是通過函數調用進行的,速度快且成本低。這些組件間的通信頻繁且緊密耦合,系統的組件通常是以源碼形式進行交付,並在部署時經過編譯和靜態鏈接。

這種有紀律的分區設計有助於提升開發、測試和部署的效率。

單體結構雖然簡單直接,但隨著系統規模增長,會逐漸顯現出令人生畏的特性:

  1. 複雜性增加:隨著功能不斷添加,整個系統變得越來越複雜,難以理解和維護。
  2. 擴展性受限:無法針對特定模組單獨進行擴展,必須擴展整個系統。
  3. 部署風險:任何小改動都需要重新部署整個系統,增加了部署的風險和困難。
  4. 技術棧限制:整個應用被綁定到同一個技術棧,難以採用新技術。
  5. 團隊協作困難:大型單體應用可能導致代碼衝突和集成問題,影響開發效率。
  6. 可靠性問題:任何模組的錯誤可能影響整個系統的穩定性。
  7. 長期維護挑戰:隨著時間推移,可能形成"大泥球",使得系統越來越難以維護和更新。

這些因素綜合起來,使得大型單體結構在長期發展中變得令人生畏,尤其是對於需要快速迭代、高度可擴展的現代應用來說更是如此。

DEPLOYMENT COMPONENTS 部署層次的組件

系統架構中的物理邊界常見形式是動態連結庫,例如 .Net 的 DLL、Java 的 jar 檔案等,這些元件在部署時以二進制形式交付,不需要重新編譯,實現了部署層次上的解耦模式。專案部署時,將所有可部署單元打包成便於操作的格式,如 WAR 檔案或目錄。

這類元件與單體結構相似,函數仍在同一程序和位址空間中,管理元件劃分和依賴關係的策略也一致。元件間的跨邊界調用是普通的函數調用,雖然動態連結或執行時載入會有一次性成本,但跨邊界通信調用仍然頻繁。

  • 簡單例子
    假設有一個簡單的線上購物系統,其中包含用戶管理、商品管理和訂單管理等功能。這些功能可以用不同的動態連結庫來實現,例如:
    • User.dll:負責用戶註冊、登入、資料管理等功能。
    • Product.dll:處理商品資訊的添加、修改、刪除等功能。
    • Order.dll:管理訂單的創建、查詢、支付等操作。

    在這個系統中,各個功能的實現被打包成獨立的 DLL 檔案,這樣的架構可以在不重新編譯整個系統的情況下更新單個功能模塊。這些 DLL 檔案在部署時作為獨立的二進制單元一起打包成最終的應用程式,例如放在同一個目錄中運行。系統內部,使用函數調用在不同 DLL 之間進行通信,例如訂單模塊需要查詢用戶資訊時,會通過普通的函數調用來訪問 User.dll 提供的功能。
    這種方式與單體結構相似,所有 DLL 在同一個程序和位址空間中運行,跨邊界的調用就像普通的函數調用一樣,調用成本很低,實現了部署層次上的解耦。

THREADS 線程

單體結構和按部署層次劃分的元件都能使用線程。

線程既不是架構邊界,也不是部署單元,而是一種管理程序執行順序和調度的方式。

一個線程可以完全包含在單一元件內,也可以橫跨多個元件。

  • 簡單例子
    假設我們有一個網上商店應用程式,它包含以下幾個主要元件:
    1. 用戶界面(UI)元件
    2. 商品目錄元件
    3. 購物車元件
    4. 訂單處理元件

    這個應用程式可能會使用多個線程來提高效能和用戶體驗:
    1. 主線程:負責處理用戶界面的更新和響應用戶操作。
    2. 商品加載線程:當用戶瀏覽商品目錄時,這個線程負責從資料庫加載商品信息,以不阻塞主線程的方式實現平滑的滾動和瀏覽體驗。
    3. 訂單處理線程:當用戶下單時,這個線程負責處理訂單,包括庫存檢查、支付處理等,讓用戶可以繼續瀏覽網站而不需等待訂單處理完成。

    在這個例子中,商品加載線程可能會跨越 UI 元件和商品目錄元件,而訂單處理線程則可能跨越購物車元件和訂單處理元件。這些線程並不受元件邊界的限制,而是根據功能需求來組織的。

LOCAL PROCESSES 本地進程

本地進程是系統架構中的一個強隔離邊界,它們有獨立的地址空間,通常通過 socket 或操作系統提供的通信工具(如消息佇列)進行交互。每個進程可以是單體結構或由動態部署的組件組成。

進程間的隔離策略類似於單體結構和二進制組件,源碼依賴指向更高層級的組件,高層進程不應直接依賴低層進程。跨進程通信的成本較高,因此需謹慎控制頻率。

  • 簡單例子
    假設我們有一個網頁應用程式,它分為前端和後端兩個部分:
    1. 前端進程:負責顯示用戶介面,處理用戶的輸入,例如使用 JavaScript 和 HTML 來渲染網頁。
    2. 後端進程:負責處理數據邏輯,例如使用 Node.js 或 Python 來處理資料庫查詢和業務邏輯。

    這兩個進程運行在不同的地址空間中,通過 HTTP 協議(如 REST API 或 WebSocket)來進行通信。前端發送請求到後端,後端處理後返回結果給前端。這樣的進程隔離讓前後端可以獨立運作並易於維護,也降低了相互間的依賴度。
    比如,當前端需要顯示用戶資料時,它不會直接去查資料庫,而是通過 HTTP 請求讓後端進程去做查詢,這樣保持了進程之間的清晰邊界和隔離。

SERVICES 服務

在系統架構中,最強大的隔離方式是使用服務。簡單來說,服務就是一個獨立運行的程式,可以用命令行或系統指令啟動,不依賴於特定的硬體或地點。

服務之間的溝通是透過網路完成的,速度會比程式內部的函數呼叫慢很多,可能需要從幾十毫秒到幾秒不等的時間。

所以,在設計時,我們應該盡量減少這些服務之間的溝通次數,並且要能夠處理網路延遲的情況。

在服務的層次上,較低層的服務應該像「插件」一樣為較高層的服務提供功能,而較高層的服務不應該知道低層服務的具體細節,例如服務的位置或網址。

這樣的設計原則有助於保持系統的靈活性和擴充能力。

  • 簡單例子
    假設你在使用一個線上購物網站,這個網站背後有多個不同的服務在支援。比方說,這個網站有一個「商品庫存服務」負責管理商品的數量,還有一個「支付服務」負責處理用戶的付款。
    網站同時連接到這兩個服務,但兩個服務之間並沒有直接的連接,反映了它們的獨立運行特性。