為“架構”再建個模:如何用程式碼描述軟體架構?

phodal 發表於 2022-05-24

在架構治理平臺 ArchGuard 中,為了實現對架構的治理,我們需要程式碼 + 模型描述所要處理的內容和資料。所以,在 ArchGuard 中,我們有了程式碼的模型、依賴的模型、變更的模型等,剩下的兩個核心的部分就是架構的模型架構的治理模型,其它的還有諸如構建的模型等,會在後續的過程中持續引入到系統中。

PS:本文裡的架構展開是基於自動化分析需求的,模型也是基於這個動機出發的。

架構是什麼??

對單個語言的程式碼建模並不難,對於一個語言有特別的概念,如 package、class、field、function 等等。在有了明確概念的基礎之下,結合我們的業務上的需求,就能構建一個太差不差的模型。在採用 DDD 這一類建模方式的時候,產生共識,提煉知識,形成概念等,便能構建出模型的雛形。

起點:架構是重要的元素

然而,對於架構來說,業內沒有統一的定義。於是乎,諸如 Martin Fowler 喜歡引用 GoF(《設計模式》作者們) 之一的 Ralph Johnson 對於架構的描述:

架構是那些重要的東西……,無論它具體是什麼。

同樣的 Grady Booch (UML 的發明者之一)也是惟類似的方式來概括架構的:

軟體架構是系統設計過程中的重要設計決定的集合,可以通過變更成本來衡量每個設計決定的重要維度。

所以呢,這讓我們感覺說了等於沒說,我們得去定義什麼是重要的東西。而重要的東西,在不同人、不同場景之下,它是存在差異的。哪怕是同一個型別的軟體,在不同的公司、不同的利益相關者的背景之下,重要的東西也盡相同。

原則:可是到底哪些是重要的?

於是乎,我再嘗試去引用最新的架構相關的書籍,諸如於我編寫這篇文章時,參考的書《軟體架構:架構模式、特徵及實踐指南》一書中 Neal Ford 對於架構的定義:

軟體架構中包含系統的結構、系統必須支援的架構特徵、架構決策以及設計原則。系統的結構是指實現該系統的一種或多種架構風格(如微服務、分層和微核心等)。架構特徵定義了系統的成功標準。架構決策定義了一組關於如何構建系統的規則。設計原則是關於如何構建系統的非必須遵循的指導原則。

從模型構築的層面來說,書中的定義也提供了一個靈活性。諸如於在架構特徵的定義裡,關注的是各類能力(ability),如互操作性、可適用性、可測試性等等。

在現有的 ArchGuard 這個業務場景之下,我們難以自動化地識別出各類的特徵。因為從實踐的層面上來說,這些能力並不一定實現了,它是目標架構,可能還只存在於架構藍圖之上的。在這個層面上來說,這裡定義的架構,偏向於是設計層面的定義。

從另外一方面來說,架構決策則是在架構治理的過程中,我們所關注的核心。可以在後續針對於這一系列的原則的規則,構建出一個描述架構特徵的 DSL。

重要的元素:元件、邊界與通訊

接著,讓我們再回到 Bob 大叔(Robert C. Martin)的《架構整潔之道》書中的定義:

軟體系統的質量是由它的構建者所決定的,軟體架構這項工作的實質就是規劃如何將系統切分成元件,並安排好元件之間的關係,以及元件之間互相通訊的方式

再從 Clean Architecture 模式來說,Bob 大叔一直在強調的是:頂層抽象策略與底層實現要實現解耦。諸如於如何劃定合理的邊界?如何組合相關的策略與層次?從這個模式上來說,我們得到了一個越來越清晰的定義。

然而,我們還遇到一個更難的問題是,如何定義一個元件是什麼?**還有關係是什麼?**在書裡的序言, Kevlin Henney(《面向模式的軟體架構》卷4、卷 5的作者之一)給了一個更精確的描述詞:組織結構(structure),從巨集觀到微觀的構築過程,其中的構件包含了元件、類、函式、模組、層級、服務等。

對於大型軟體來說,其組織結構方式異常複雜,它像極了一個國家的層級關係,一級部門、二級部門等等。而部門之間又有複雜的關係,正是層級關係 + 層級的構件構建成了這個複雜的系統。(PS:而了讓系統能良好的執行,即其中的元件(螺絲釘)按規則執行,則需要一個督察組織。)

層次結構:元件和關係

軟體架構已經有了幾十年的歷史,我們已經用 ”模式“ 這一詞對過去的架構進行了一系列的總結。二十年前,人們初步總結了《面向模式的軟體架構》(POSA)。在這裡,就引述 POSA 1 的第 6 章裡,有一個完整的層級關係介紹:

軟體架構描述了軟體系統的子系統和元件以及它們之間的關係。通常使用不同的檢視來說明子系統和元件,以展示軟體系統的功能特徵和非功能特徵。

元件是被封裝起來的軟體系統的一部分,包含一個介面。元件是用於打造系統的構件。在程式語言層面,元件可能由模組、類、物件或一組相關的函式表示。

關係描述了元件之間的聯絡,可能是靜態的,也可能是動態的。靜態關係會在原始碼中直接顯示出來,它們指出了架構中元件的佈局;動態關係指出了元件之間 的臨時關係和動態互動,可能不容易通過原始碼的靜態結構看出來。

檢視呈現軟體架構的某個方面,展示軟體系統的某些具體特徵。

……

在今天來看,從模式上來說,軟體架構本身並沒有發生太大的變化。只是呢,一些定義發生了變化,諸如於元件和介面。在微服務架構風格流行的今天,一個微服務也可以視為一個元件,它包含了一系列的介面,對外提供了複用的能力。而用來描述它們的關係的元素,則不再是過去的函式呼叫,變為了遠端呼叫、事件觸發。

現在,我們有了一詳盡的定義,在建模上,可能還欠缺一些元素,諸如於,如何分析出元件間的關係

第 3 種架構檢視:展示工程關注點

在 ArchGuard 中,我們使用了 C4 架構視覺化模型作為一種參考檢視。這種實現的方式主要是從分析和視覺化的層面來考慮的。除了 C4 之外,另外一種主流的方式是 4 + 1 檢視。順帶一提,在 4 + 1 的論文《Architectural Blueprints—The “4+1” View Model of Software Architecture》,同樣也有一個描述架構的表示公式:Software architecture = {Elements, Forms, Rationale/Constraints}

從通識的角度來看,採用 4 + 1 檢視是一個比較理想的方式。只是,由於存在大量的 PaaS、IaaS 等 xx 即服務設計的不合理性,使得這些記錄基礎設計相關資訊的程式碼,並沒有與程式碼庫一起存放,使得在辯識上存在一定的難度。

因此,從實現的層面來說,在這裡,我們要引用的是《面向模式的軟體架構》中,提到的《Software Architecture in Industrial Applications》(也可以參考《實用軟體體系結構》一書)架構檢視:

  • 概念檢視:描述了整個系統需求向整個體系結構的轉化。
  • 模組檢視:描述瞭如何將系統劃分成模組並將模組組織成層。
  • 執行檢視:描述了系統的動態元素以及它們之間的互動。
  • 程式碼檢視:描述了原始碼的組織結構。

在這個檢視的定義裡,它更能清晰的劃分開幾個不同層面的考慮因素。採用作者們在最早的論文裡提到的示例:

軟體架構使用示例影響因素的例子
程式碼架構配置管理, 系統構建、OEM 定價程式語言,開發工具和環境,擴充套件子系統
模組架構模組介面管控、變更影響分析、介面約束一致性檢查、配置管理使能軟體技術、組織結構、設計原則
執行架構效能和可排程性分析,系統的靜態和動態配置,將系統移植到不同的執行環境硬體架構、執行時環境效能標準、通訊機制
概念架構使用特定領域的元件和聯結器進行設計、效能評估、安全性和可靠性分析、瞭解系統的靜態和動態可配置性應用領域、抽象軟體正規化、設計方法

從表格的右邊裡,我們就可以直接對應到系統所需要的每個層面的設計因素,諸如於程式語言等元素放在程式碼架構上。換句話來說,在微服務、單體架構下,都能找到自己合適的位置。

概念的最後:描述模型的型別系統

最後,為了保證本文在概念的完整性,我們還需要一種方式來描述這個系統種的模型和一系列的概念,從形式上來說,它是一個型別系統。諸如於,我們在 UML中所表示的 (PlantUML 表示方式):

class Architecture {
   Component[] components
   System[]    subSystems
   Relation[]  relations
   ArchStyle   archStyle
   Rule[]      archRules
   ...
}

一個用來描述型別的系統,就是一個型別系統,和程式語言裡的型別是等同的。它可以用來解釋一系列的概念,以及概念之間如何連線。

順帶一題,如果我們把程式語言看作是一個系統,那麼我們就會發現其在設計的有趣之處。型別系統與結構體(或者類)可以用於構建系統中的概念,一個個的表示式則是用於構建概念之間的關係。

建模

對於概念來說,哪怕是有了如此多豐富的展開,要做一個小結也並非一件容易的事情。更何況於模型而言,它是數值上的標準化,一種接近於通用的模型形態。

設計:第一個版本的架構模型

所以,我的第一個嘗試是從 《面向模式的軟體架構》的定義下手:

class SystemArchitecture(
    val archStyle: ArchitectureStyle,
    val subSystem: List<SubSystem>,
    var components: List<ArchComponent>,
    val connections: List<ArchConnection>
)

示例:對於一個系統來說,它存在一個主的架構模式,如微服務架構。而在每個微服務相當於是一個子系統或者說元件。當然了,一個子系統可以包含多個微服務。而對於個元件來說,它包含了輸入和輸出,以及一系列的計算邏輯。所以,它的一個模型可能是這樣的:

class ArchComponent(
    val name: String,
    val type: ArchComponentType,
    val inbounds: List<String>,
    val outbound: List<String>,
    val components: List<ArchComponent>
)

而麻煩的一點在於,元件是動態的,元件之間是存在關係的,元件內部也存在關係。所以,我們還需要考慮如何表示元件間的關係?

class Connection(
    val connectors: String,
    val source: String,
    val target: String,
    var connectionType: ConnectionType,
    val connectorStyles: ConnectorStyle,
)

所以,如果以寬泛的定義來看,元件其實是樹形結構的。同樣的,對於架構來說,這種 Connection 也是存在的。根據上面的一系列形態展開,我們就能得到一個初始版本的架構的架構模型。

更多模型相關內容,詳見 ArchGuard Scanner 中的初始版本 Architecture.kt

實現思路:生成架構模型

ArchGuard 中的模型是通過程式碼來生成的。在這種業務場景之下,在構建模型的時候,需要平衡設計與實現。由此,基於程式碼分析的檢視方式更適用我們。從程式碼和依賴中,我們可以:

  • 基於目錄結構分析出分層關係,進而得到程式碼的結構檢視。
  • 分析依賴管理工具,進而得到一個模組的檢視。
  • 分析依賴的軟體,如 Spring、Dubbo 等,進而得到一個執行的檢視。
  • 分析軟體中的模型,進而得到概念檢視。

結合之下,我們就能構建出一個更完整的架構模型。

實現:放棄第一個版本的模型

設計挺美好的,但是當我開始寫第一行程式碼的時候,發現不對勁了:我們有了一個分析的模型,但是輸入呢?

僅從專案的分析來說,我們拿到的只是一個程式碼倉庫的程式碼。一個程式碼倉庫可能是一個模組,一個系統,又或者是一個服務。所以,我們的輸入是什麼?基於已有的分析情況,如專案依賴(依賴管理工具)、原始碼結構、語言等?我們缺少構建一個專案所需要的上下文。

我們從原始碼所能分析到的內容如下所示:

data class PotentialExecArch(
    var protocols: List<String> = listOf(),
    var appTypes: List<String> = listOf(),
    var connectorTypes: List<ConnectorType> = listOf(),
    var coreStacks: List<String> = listOf(),
    var concepts: List<CodeDataStruct> = listOf()
)

對應的一些分析細節:

  1. 協議資訊。解析原始碼和 Gradle、Maven、NPM 中的依賴資訊,如含 Dubbo 就視為帶 RPC;如帶 java.io.File 就認為帶 FileIO
  2. 應用型別。解析依賴資訊,如包含 clikt 就視為 Kotlin 的 CLI 應用。
  3. 連線型別。同上
  4. 核心技術棧。根據依賴型別分析。
  5. 概念模型。需要先分析潛在的分層架構模型,根據分層架構模型,就能得到概念集。

當然了, 這部分程式碼還沒寫完,如果有興趣,也歡迎來加入我們。

多種架構模型

於是,在 ArchGuard 的背景下,“架構” 的架構模型有多種形態的:

  • 掃描形態下的架構模型(對應到 ArchGuard Scanner)。即,從各個專案的原始碼中得到的架構模型,從原始碼、依賴、資料庫等中計算出來。
  • 分析形態下的架構模型(對應到 ArchGuard Backend)。基於分析形態下的架構模型,構建出包含架構相關知識的架構模型。
  • 展示形態下的架構模型(對應到 ArchGuard Frontend)。結合視覺化的知識,展示出對應的架構模型。

每一個模型都是在上一步的基礎上新增了領域知識,那麼什麼是領域知識呢?如何做好架構?

小結:挑戰

模型,一個開始,它需要不斷地演進才能適用需要。而有了一個湊合能用的模型,只是這一系列工作的開始,我們還會遇到一系列的挑戰。

描繪目標架構

系統中的架構反應的只是現狀,如何去描述未來的架構,並將兩者進行匹配,又是一個非常有意思的話題。

衡量變化性

我們還將面臨的另外一個問題是,軟體架構並非是不變的。

軟體只要一直在開發,就會以細微地方式變化著。從巨集觀的層面來說,儘可能架構師都在努力地不去大範圍地變動結構。而我們需要面對於這些挑戰,諸如於基礎設施變化,而這種變化帶來的是臨時性的中間狀態。

如何去表示這種臨時性的中間狀態,就變得更加有意思。

在這裡只開始了邁出了第一步,如果大家有興趣,歡迎來 ArchGuard 為技術建模:https://github.com/archguard/archguard/discussions/67

參考資料

  • 《整潔架構之道 》
  • 《架構之美》
  • 《軟體架構:架構模式、特徵及實踐指南》
  • 《面向模式的軟體架構 卷 1:模式系統》
  • 《實用軟體體系結構》
  • 《Software Architecture in Industrial Applications》
  • 《Architectural Blueprints—The “4+1” View Model of Software Architecture》