軟體架構設計的挑戰之一是確保系統滿足現在和未來的需求。適應度函式提供了一種驗證架構完整性的方法,而軟體度量則幫助評估軟體的品質和維護性。本文將探討如何結合這兩種方法來構建更健壯、更易維護的軟體系統。從分層架構驗證到微服務互動的適應度函式,以及 GitHub 的忠實度適應函式案例研究,本文提供了一系列實踐和案例分析,幫助讀者理解如何在實際專案中應用這些概念。此外,本文還探討了軟體度量的重要性,特別是如何利用度量來識別和解決迴圈依賴等架構問題,最終提升軟體的整體品質和可維護性。

建築師的適應度函式:從度量到工程的進化

軟體架構設計是一個不斷演進的過程,建築師需要確保系統架構能夠滿足當前和未來的需求。適應度函式(Fitness Functions)提供了一種有效的方法來驗證和確保架構的完整性。本文將探討適應度函式的概念、應用場景以及如何在不同的架構中使用它們。

分層架構驗證

在軟體開發中,分層架構是一種常見的設計模式,用於分隔系統的不同部分。例如,在一個典型的企業應用程式中,可能會有控制器(Controller)、服務(Service)和持久層(Persistence)等不同的層。確保這些層之間的正確互動對於系統的穩定性和可維護性至關重要。

使用 ArchUnit 進行分層架構驗證

ArchUnit 是一個用於 Java 的函式庫,允許開發者編寫測試來驗證架構的規則。例如,可以使用 ArchUnit 編寫一個測試來確保控制器層不被其他層直接存取,服務層不被控制器層以外直接存取等。

layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Persistence").definedBy("..persistence..")
    .whereLayer("Controller").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayNotBeAccessedByAnyLayer("Controller")
    .whereLayer("Persistence").mayNotBeAccessedByAnyLayer("Service")

內容解密:

此段程式碼定義了一個分層架構,並驗證了各層之間的依賴關係。其中:

  • layeredArchitecture() 初始化了一個分層架構的驗證。
  • .layer("Controller").definedBy("..controller..") 定義了一個名為 “Controller” 的層,並指定該層的類別應位於 ..controller.. 包下。
  • .whereLayer("Controller").mayNotBeAccessedByAnyLayer() 驗證 “Controller” 層不被其他層直接存取。
  • 類別似的規則被應用於 “Service” 和 “Persistence” 層,以確保正確的依賴關係。

分散式架構中的適應度函式

在微服務架構中,服務之間的互動變得更加複雜。確保服務之間的正確互動對於系統的穩定性至關重要。

微服務拓撲範例

考慮一個微服務架構,其中一個協調器服務(Orchestrator)負責協調其他三個領域服務(Domain Services)。建築師希望確保領域服務之間不直接互動,而是透過協調器服務進行互動。

  graph LR
    A[協調器服務] --> B[訂單服務]
    A --> C[支付服務]
    A --> D[庫存服務]
    B -->|禁止直接互動|> C
    B -->|禁止直接互動|> D
    C -->|禁止直接互動|> D

圖表翻譯:
此圖表展示了一個微服務架構,其中協調器服務與三個領域服務(訂單服務、支付服務和庫存服務)進行互動。領域服務之間禁止直接互動,確保系統的正確協調。

驗證微服務互動的適應度函式

由於微服務架構的複雜性,現有的工具可能無法直接滿足所有驗證需求。因此,建築師需要自定義適應度函式來驗證服務之間的互動。

def ensure_domain_services_communicate_only_with_orchestrator():
    list_of_services = ["協調器服務", "訂單服務", "支付服務", "庫存服務"]
    for service in list_of_services:
        logs = import_logs_for(service, 24)
        for call in logs:
            if call.destination != "協調器服務":
                raise FitnessFunctionFailure()

內容解密:

此段程式碼實作了一個適應度函式,用於驗證領域服務是否只與協調器服務進行互動。其中:

  • list_of_services 列出了系統中的所有服務。
  • import_logs_for(service, 24) 匯入每個服務過去24小時的互動日誌。
  • 遍歷每個服務的日誌,檢查其互動目標是否為協調器服務。如果發現任何服務直接與其他領域服務互動,則引發 FitnessFunctionFailure

零日安全檢查案例研究

2017 年,Equifax 發生了一起嚴重的安全漏洞,原因是未及時修補 Apache Struts 的已知漏洞。這個案例凸顯了持續監控和自動化安全檢查的重要性。

佈署Pipeline中的安全檢查

假設每個專案都有一個佈署Pipeline,並且安全團隊可以在Pipeline中加入自定義的安全檢查。這樣,當新的零日漏洞出現時,安全團隊可以快速插入檢查邏輯,自動化地檢查所有專案中是否存在受影響的元件。

  graph TD
    A[原始碼] --> B[建置]
    B --> C[測試]
    C --> D[安全檢查]
    D --> E[佈署]

圖表翻譯:
此圖表展示了一個包含安全檢查階段的佈署Pipeline。安全檢查階段允許安全團隊在Pipeline中加入自定義的安全檢查邏輯,以確保系統的安全性。

案例研究:忠實度適應函式(Fidelity Fitness Functions)

在軟體架構領域中,如何確保新舊系統之間的忠實度(fidelity)是一項重大挑戰。所謂的忠實度,是指新系統與舊系統在功能和行為上的一致性。GitHub工程團隊在其部落格上分享了一個名為「Move Fast and Fix Things」的案例研究,展示瞭如何利用忠實度適應函式來解決這一問題。

GitHub的案例研究

GitHub是一傢俱有高度工程敏捷性的公司,其工程團隊採用持續佈署(Continuous Deployment)的做法。開發人員的每次程式碼變更都會透過佈署Pipeline(deployment pipeline),只要沒有錯誤,就會被佈署到生產環境。GitHub平均每天進行60次佈署,但也因此面臨著大量的邊緣案例(edge cases)。

在一次案例研究中,GitHub的工程團隊決定改進其合併(merge)功能的實作方式。原有的合併功能是透過一個shell指令碼來實作的,雖然運作良好,但可擴充套件性不佳。因此,團隊決定開發新的根據記憶體的合併功能,並進行了大量測試以確保其正確性。

然而,將新功能佈署到生產環境是一項高風險的工作。團隊擔心新功能可能會引入未知的錯誤,特別是與舊功能之間的耦合(coupling)問題。為瞭解決這一問題,GitHub團隊開發了一個名為Scientist的工具,允許開發人員在不影響使用者的情況下進行實驗。

Scientist工具的運作原理

Scientist工具允許開發人員建立實驗,每個實驗包含兩個子句:usetryuse子句包含舊程式碼,而try子句包含新程式碼。在合併功能的實驗中,create_merge_commit方法被封裝在一個科學實驗(science block)中,如下所示:

def create_merge_commit(author, base, head, options = {})
  commit_message = options[:commit_message] || "Merge #{head} into #{base}"
  now = Time.current
  science "create_merge_commit" do |e|
    e.context :base => base.to_s, :head => head.to_s, :repo => repository.nwo
    e.use { create_merge_commit_git(author, now, base, head, commit_message) }
    e.try { create_merge_commit_rugged(author, now, base, head, commit_message) }
  end
end

在這個例子中,science區塊充當了usetry兩個子句的分派器。每次呼叫時,use子句總是會被執行,並且其輸出總是會被傳回給使用者。因此,使用者永遠不會意識到他們正在參與一個實驗。同時,架構師可以組態框架以決定多久執行一次try區塊——在合併實驗中,它執行了1%的請求。

usetry都執行時,框架會:

  • 隨機化usetry的執行順序,以防止時序異常(timing anomalies)
  • 比較兩個呼叫的結果,以檢查忠實度
  • 捕捉並記錄try區塊中拋出的任何異常
  • 將結果釋出到儀錶板(dashboard)上,如圖8-9所示

實驗結果

圖8-9顯示了GitHub在某個時間段內進行的合併操作次數。然而,由於GitHub的規模龐大,錯誤並不容易在這個檢視中被察覺。圖8-10顯示了同一時間段內的錯誤情況。

  graph LR
    A[開始實驗] --> B{執行use和try子句}
    B -->|使用use子句|> C[傳回結果給使用者]
    B -->|使用try子句|> D[比較結果]
    D -->|結果一致|> E[記錄成功]
    D -->|結果不一致|> F[記錄錯誤]
    F --> G[通知開發團隊]

圖表翻譯:
此圖表展示了Scientist工具的運作流程。首先,實驗開始並執行usetry子句。use子句的結果會傳回給使用者,而try子句的結果則用於與use子句的結果進行比較。如果結果一致,則記錄成功;如果結果不一致,則記錄錯誤並通知開發團隊。

實驗結果分析

如圖8-10所示,新程式碼中存在一些錯誤。然而,由於Scientist框架的存在,使用者並沒有受到這些錯誤的影響。相反,開發人員修復了問題並重新佈署——在這個實驗期間,持續佈署繼續進行,不僅是針對這個實驗,也包括其他程式碼。

實驗的一個重要目標是提高效能,從圖8-11的圖表中可以看出,效能確實得到了改善。GitHub的架構師們進行了為期四天的實驗,直到他們獲得了24小時內沒有任何不匹配或緩慢情況的結果。最終,他們移除了舊的合併程式碼,保留了新的程式碼。在這四天中,他們進行了超過1000萬次實驗,從而對新程式碼的正確性充滿信心。

Scientist:一個忠實度適應函式

Scientist是一個典型的忠實度適應函式,透過功能切換(feature toggles)和效能指標來實作。這種方法展示了工程和指標之間的協同作用如何為專案帶來超能力。

忠實度適應函式的重要性

忠實度適應函式在現代軟體開發中扮演著至關重要的角色。它們不僅能夠幫助團隊確保新舊系統之間的忠實度,還能夠提高系統的穩定性和可靠性。透過結合工程實踐和指標,團隊可以建立一個強大的驗證機制,從而減少錯誤並提高整體的開發效率。

隨著軟體系統變得越來越複雜,忠實度適應函式的重要性將會進一步凸顯。未來的軟體開發將更加依賴於自動化和智慧化的驗證機制,以確保系統的正確性和穩定性。因此,開發和完善忠實度適應函式將成為軟體工程領域的一個重要研究方向。

忠實度適應函式的最佳實踐

  1. 明確定義忠實度要求:在進行系統改進或升級時,明確定義新舊系統之間的忠實度要求。
  2. 選擇合適的工具:選擇適合的工具來實作忠實度適應函式,如Scientist。
  3. 持續監控和改進:持續監控系統的執行情況,並根據需要改進忠實度適應函式。
  4. 整合到CI/CD流程:將忠實度適應函式整合到持續整合和持續佈署(CI/CD)流程中,以確保系統的穩定性和可靠性。

透過遵循這些最佳實踐,團隊可以有效地利用忠實度適應函式來提高軟體系統的品質和可靠性。

軟體度量在維護性確保中的重要性

在軟體開發領域,確保軟體的維護性是一項至關重要的任務。軟體度量(Software Metrics)作為一種有效的工具,可以幫助開發團隊評估軟體的品質、架構完整性以及維護難度。本章將介紹幾種關鍵的軟體度量方法,並探討如何利用這些度量來提升軟體的維護性,降低開發與維護成本,以及減少專案風險。

為什麼需要軟體度量

幾乎所有生產複雜產品的行業都依賴度量來確保產品的品質和可用性。現代製造業如果沒有嚴格的品質測量標準,將難以想像。然而,軟體行業在這方面明顯落後,儘管軟體開發過程非常需要這樣的方法。

實施軟體度量的最佳方式是建立一個根據度量的反饋迴圈(Metrics-based Feedback Loop)。這種方法能夠確保軟體產品達到可衡量的品質標準,不僅能夠提升整體品質,還能提高軟體的維護性,從而提高開發者的生產力。更好的維護性意味著程式碼更容易閱讀和理解,開發者可以將更多時間投入到改進或新增功能上,而不是花在閱讀程式碼上。

圖表 9-1:根據度量的反饋迴圈

  graph LR
    A[定義可量化的目標] --> B[實施產品開發]
    B --> C[持續驗證是否達到目標]
    C -->|是| B
    C -->|否| D[改進實施方案]
    D --> B

圖表翻譯: 上圖展示了一個根據度量的反饋迴圈。在這個迴圈中,首先定義可量化的目標,然後在產品開發過程中持續驗證是否達到這些目標。如果沒有達到目標,則改進實施方案,直到達到目標後繼續開發。

熵如何摧毀軟體系統

在開發非平凡軟體系統時,最大的敵人是熵(Entropy),也稱為結構侵蝕(Structural Erosion)。結構侵蝕的最終狀態是臭名昭著的「大泥球」(Big Ball of Mud),這代表著一個架構混亂、高度耦合的程式碼函式庫,各個部分之間存在大量不必要的依賴關係。典型的症狀包括:在系統的一個部分進行修改可能會導致完全無關的部分出現問題。

另一個明顯的症狀是大量的迴圈依賴(Cyclic Dependencies),形成巨大的迴圈群組(Cycle Groups)。軟體度量在測量熵方面非常有效,這使得它們成為緩解這一問題的理想工具。

圖表 9-2:迴圈群組視覺化

  graph TD
    A[A]
    B[B]
    C[C]
    D[D]
    E[E]
    F[F]
    A --> B
    B --> C
    C --> A
    D --> E
    E --> F
    F --> D

圖表翻譯: 上圖展示了迴圈群組的概念。圖中的節點可以代表原始檔案、名稱空間或軟體系統的其他組成部分。箭頭表示這些元素之間的依賴關係。在這個例子中,有兩個迴圈群組,分別以不同的灰色標示。白色節點不參與任何迴圈依賴。

迴圈依賴的危害

為什麼迴圈依賴是一個嚴重的問題?儘管像 Apache Cassandra 這樣的專案看似運作正常,但迴圈依賴使得無法單獨測試程式碼的某個部分,也讓新開發者難以理解程式碼,因為一個隨機選取的原始檔案可能直接或間接依賴於幾乎所有其他檔案。

另一個問題是,緊密耦合使得無法隔離或替換某些功能,而不進行風險高、耗時長的全域性更改。這使得模組化變得不可能。可以說,開發者將 Cassandra 的架構圖簡化為了一個標有「Cassandra」的大黑箱。雖然這樣簡化後的圖表易於閱讀,但它無法揭示軟體的內部結構。

如何解決迴圈依賴

幸運的是,我們可以打破所有的迴圈依賴。例如,可以應用由 Robert C. Martin 提出的「依賴反轉原則」(Dependency Inversion Principle)。透過引入介面,可以反轉迴圈群組中的依賴關係,通常能夠打破迴圈。此外,還有其他幾種技術可以打破迴圈,例如將迴圈依賴提升到更高層級的類別,或者將某些功能在類別之間移動以打破迴圈。

程式碼範例:依賴反轉原則

// 原始程式碼:存在迴圈依賴
public class A {
    private B b;
    public A() {
        b = new B(this);
    }
}

public class B {
    private A a;
    public B(A a) {
        this.a = a;
    }
}

// 應用依賴反轉原則後的程式碼
public interface InterfaceA {
    void methodA();
}

public class A implements InterfaceA {
    private InterfaceB b;
    public A(InterfaceB b) {
        this.b = b;
    }
    @Override
    public void methodA() {
        // 實作
    }
}

public interface InterfaceB {
    void methodB();
}

public class B implements InterfaceB {
    private InterfaceA a;
    public B(InterfaceA a) {
        this.a = a;
    }
    @Override
    public void methodB() {
        // 實作
    }
}

內容解密:

上述範例展示瞭如何透過依賴反轉原則來打破迴圈依賴。原始程式碼中,類別 A 和 B 之間存在迴圈依賴。透過引入介面 InterfaceAInterfaceB,我們能夠將依賴關係反轉,從而打破迴圈。這種方法不僅提高了程式碼的可測試性,還增強了模組化能力,使得系統更易於維護和擴充套件。