將架構作爲語言:一個故事

通常,架構要麼是在Word文檔中描述的一些軟件系統中無形的、概念性的方面,要麼就完全是由技術驅動的(“我們使用了一個XML架構”)。這兩種方式都很糟糕:前者很難派上用場,而後者架構上的概念被技術宣傳所掩蓋。

什麼纔是好的表達?應該是隨着架構的發展,演化出一門語言,讓你得以從架構的角度來描述系統。根據我在多個真實項目中獲得的經驗,這種表達方式能夠 形象、無歧義地描述架構構建模塊和具體系統,同時又不至於深入到技術決策的細節(技術決策應該有意識地放到另一個單獨的步驟中)。

本篇論文的第一部份通過一個真實故事演示了這一思想。第二部分則總結了這一方法的關鍵點。

一個故事

系統背景

我正與一位客戶在一起,他是我負責定期諮詢工作中的其中一位客戶。客戶決定構建一個全新的航空管理系統。航空公司使用該系統跟蹤和發佈不同的信息, 如:飛機是否降落在指定的機場;航班是否延遲;飛機的技術狀態等等。系統同時還要爲Internet的在線跟蹤系統以及在機場等地設置的信息監控器提供數 據。無論從哪個方面來看,該系統都屬於一個典型的分佈式系統,系統的各個部分分別運行在不同的機器上。它有一箇中央數據中心負責處理繁重的數字運算,還有 其他機器分佈放置在相對廣闊的區域中。多年來,我的客戶一直在構建類似這樣的系統,現在他們計劃引入新一代的系統。新系統必須能夠支持15-20年時間的 演進。單單從這一項需求就可以清楚地看出,他們需要對技術進行某種抽象,因爲在這15-20年期間可能要經歷8次技術潮流的變遷。對技術進行抽象還有另一 個重要的理由,那就是系統的不同部分採用了不同的技術來構建,有Java,C++,C#。採用多種技術對於大型分佈式系統而言並非特殊的需求。通常,我們 會在後端使用Java技術,而在Windows前端使用.NET技術。

由於系統的分佈式本質,不可能在同一時間更新系統的所有組成部分。這就產生了另一項需求,就是能夠一部分一部分地更新該系統。這就反過來要求能夠管理不同系統組件之間的版本衝突問題(確保組件A在組件B被升級到一個新的版本之後,仍然能夠與之協作)。

起點

在我進入項目的時候,他們已經決定系統的主幹應該是一個基於消息傳遞的基礎架構(對於這類系統而言,這是一個不錯的決策),並且他們評估了不同的消 息傳遞主幹在性能和吞吐量方面的表現。他們已經確定了在整個系統中使用一個業務對象模型,對系統操作的數據進行描述(對於這類系統而言,這實際上不是一個 好的決策,但它不影響這個故事的結論)。

因此,當我進入項目後,他們向我簡要地介紹了系統的所有細節,以及他們已經做出的架構決策,然後詢問我這些決策是否正確。但是我很快就發現,雖然他們瞭解了很多需求,也已經在架構的某些方面做出了細緻的決策,但是卻沒有形成我所說的一致的架構(consistent architecture):即對組成實際系統構建模塊的定義,也就是定義系統中的各種事物。他們沒有掌握談論這個系統的語言

實際上,這只是我進入項目時的一個初步印象。當然,我認爲該項目存在一個巨大的問題:如果你並不知道組成系統的各種事物,就很難一致地談論和描述該系統,當然更無法一致地構建該系統。你需要定義一門語言。

背景:這門語言是什麼?

當你擁有一門語言,並能夠從架構的角度談論系統時,你就擁有了一個一致的架構1。那麼語言應該是什麼樣的呢?顯然,它首先並且至少是一套定義良好的術語。定義良好首先意味着所有的利益相關者都要認同術語的含義。如果從非正式的角度來看,術語和術語的含義可能就足以定義一門語言了。

然而——這裏可能顯得有些突然——我一向鼓吹的是要用一門正式語言來描述架構2。要定義一門正式語言,你需要的不僅僅是術語和術語的含義。你還需要一種語法來描述如何通過這些術語組成“語句”(或者模型),同時需要一種具體的句法去表示它們3

使用一門正式的語言來描述你的架構,會帶來許多好處,隨着故事的逐漸展開,這些好處也會展露無遺。同時,在本文的末尾我會對其進行總結。

發展出一門語言以描述架構

讓我們繼續這個故事。我的客戶與我都同意值得花上一天的時間去審閱某些技術需求,併爲架構建立一門正式語言來體現這些需求。實際上,我們一邊討論整個架構,一邊構建出語法、某些約束以及一個編輯器(使用oAW的Xtext工具)。

開始

我們首先從組件的概念開始。我們對組件概念的定義是相對比較寬鬆的。它只是與架構相關的構建模塊的最小單元,封裝了應用程序的功能。同時,我們假定 組件是能夠被實例化的,以便使架構中的組件概念對應上OO編程中的類。因此,根據我們定義的初始語法,首先構建的模型應該是這樣:

 DelayCalculator {}
InfoScreen {}
AircraftModule {}

注意,在這裏我們做了兩件事情:我們首先定義了系統中存在組件的概念(使得組件成爲我們要構建的系統的構建模塊),其次我們還(初步)決定系統中存在三個組件DelayCalculatorInfoScreenAircraftModule。我們爲架構提出了一套構建模塊,作爲一個概念型的架構,並將這些構建模塊的一套具體範本作爲應用程序架構4

接口

當然,上述關於組件的概念並無太大用處,因爲組件無法交互。領域邏輯清晰地表明,DelayCalculator必須接收來自AircraftModules的消息,從而計算航班的延誤狀態,然後將結果轉發給InfoScreens。我們知道,它們應該以某種方式交換信息(記住:已經作出了消息傳遞決策)。但是,我們決定不引入消息,而是將一組相關的消息抽象爲接口5

 DelayCalculator  IDelayCalculator {}
InfoScreen IInfoScreen {}
AircraftModule IAircraftModule {}
IDelayCalculator {}
IInfoScreen {}
IAircraftModule {}

我們認識到,上面的代碼看起來有幾分像是Java代碼。無需驚訝,既然我的客戶具有Java背景,那麼系統的首選目標語言自然就是Java。因此, 我們就要從他們習慣使用的語言中,抽取出廣爲人知的概念衍生爲我們自己的語言。然而,我們很快注意到這樣的表示方式沒有太大用處:我們無法表示組件“使用 了某個特定的接口(與提供接口相對)”。瞭解一個組件需要哪些接口是很重要的,因爲我們希望能夠了解(而且之後要用工具進行分析)組件具有的依賴關係。這 對於任何一個系統都很重要,而對於版本管理的需求而言,則尤爲重要。

因此,我們對語法稍加修改,支持如下的表達形式:

 DelayCalculator {
IDelayCalculator
IInfoScreen
}
InfoScreen {
IInfoScreen
}
AircraftModule {
IAircraftModule
IDelayCalculator
}
IDelayCalculator {}
IInfoScreen {}
IAircraftModule {}

描述系統

那麼,我們來看看這些組件是如何被使用的。我們清晰地認識到組件需要支持實例化。很顯然,系統中有許多架飛機,每架飛機都運行了一個AircraftModule組件,而InfoScreens的實例數量更多。不夠明確的是我們是否需要多個DelayCalculators,但我們決定推遲對它的討論,先處理實例化的問題。

因此,我們需要能夠表示組件的實例化。

 screen1: InfoScreen
screen2: InfoScreen
...

接着,我們討論瞭如何把系統的各實例“接上線”:如何表示某個InfoScreen與某個DelayCalculator“交談”?我們必須找出某種方式來表示實例之間的關係。由於這兩個類型各自具有了“可兼容”的接口,因此,DelayCalculator可以與InfoScreen“交談”。但是暫時還難以把握這種“交談”關係。我們還注意到一個DelayCalculator實例通常會與多個InfoScreen實例“對話”。因此,我們必須以某種方式在語言中引入下標來表示實例的個數。

經過幾番修改,我引入了端口(Port)的概念(實際上在組件技術以及UML中,這是一個衆所周知的概念,但是 相對於我的客戶而言,卻是一個新名詞)。端口是在組件類型上定義的一個通信端點,當擁有端口的組件被實例化時,端口也會一同被實例化。因此,我們對組件描 述語言進行重構,以支持如下的表示形式。端口通過providesrequires關鍵字進行定義,緊接着是端口的名稱和下標,一個冒號以及與端口相關聯的接口。

 DelayCalculator {
default: IDelayCalculator
screens[0..n]: IInfoScreen
}
InfoScreen {
default: IInfoScreen
}
AircraftModule {
default: IAircraftModule
calculator[1]: IDelayCalculator
}

以上模型表示,任何一個DelayCalculator實例都要連接多個InfoScreens。從DelayCalculator實現代碼的角度來看,通過screen端口可以訪問到一組InfoScreen。而AircraftModule則只能與一個DelayCalculator“對話”,正如下標[1]所示。

新的接口標識啓發了我的客戶對IDelayCalculator進行了修改,因爲他們注意到對於不同的通信對象,應該有不同的接口(因此還應該有不同的端口)。我們對應用程序架構作出瞭如下修改:

 DelayCalculator {
aircraft: IAircraftStatus
managementConsole: IManagementConsole
screens[0..n]: IInfoScreen
}
Manager {
backend[1]: IManagementConsole
}
InfoScreen {
default: IInfoScreen
}
AircraftModule {
calculator[1]: IAircraftStatus
}

注意,端口的引入改善了應用程序架構,因爲我們擁有了體現角色的接口(IAircraftStatus,IManagementConsole)。

現在,我們擁有了端口,因此我們能夠命名通信端點。這就使得我們能夠輕而易舉地描繪出系統:互連的組件實例。注意,引入了新的結構connect

 dc: DelayCalculator
screen1: InfoScreen
screen2: InfoScreen

dc.screens (screen1.default, screen2.default)

保持大局觀

當然,從某種情況來看,爲了不至於混淆所有的組件、實例和連接器(connectors),我們無疑需要引入某種命名空間的概念。自然,我們也可以將這些內容分別放到不同的文件中(工具支持保證了“轉到定義”和“查找引用”仍然正常)。

 com.mycompany {
datacenter {
DelayCalculator {
aircraft: IAircraftStatus
managementConsole: IManagementConsole
screens[0..n]: IInfoScreen
}
Manager {
backend[1]: IManagementConsole
}
}
mobile {
InfoScreen {
default: IInfoScreen
}
AircraftModule {
calculator[1]: IAircraftStatus
}
}
}

當然,將組件和接口的定義(本質上是類型的定義)與系統的定義(連接的實例)分開,是一個很好的想法,因次,我們如下定義了一個系統:

 com.mycompany.test {
testSystem {
dc: DelayCalculator
screen1: InfoScreen
screen2: InfoScreen
dc.screens (screen1.default, screen2.default)
}
}

在一個真實的系統中,DelayCalculator必須能夠在運行時動態地發現所有可用的InfoScreens。手動地描述這些連接是沒有什麼意義的。因此,我們需要繼續前進。我們定義了一個查詢,它可以採用naming/trader/lookup/registry的基礎架構在運行時執行。每隔60秒,查詢會被執行一次,查找任何上線的InfoScreens。

 com.mycompany.production {
dc: DelayCalculator

dc.screens 60 {
type = IInfoScreen
status = active
}
}

可以使用相似的辦法實現負載均衡或者容錯能力。一個靜態的連接器能夠指向一個主要實例以及備份實例。或者,在當前使用的組件實例變爲不可用時,可以重新執行一個動態查詢。

爲了支持實例的註冊,我們在它們的定義中添加了額外的語法。一個registered的實例會在註冊記錄中使用自己的名稱(通過命名空間識別)以及所有提供的接口,自動註冊其本身。還可以指定額外的參數,如下的例子就爲DelayCalculator註冊了一個主要的實例和一個備份的實例。

 com.mycompany.datacenter {
instance dc1: DelayCalculator {
parameters {role = primary}
}
instance dc2: DelayCalculator {
parameters {role = backup}
}
}

第二部分,接口

至今我們仍然沒有真正定義一個接口究竟是什麼。我們知道,我們更願意基於一個消息傳遞的基礎架構來構建系統,因此,接口顯然必須定義爲消息的集合。於是就有了我們最初的想法:一組消息的集合,其中每條消息都有名稱,以及一組類型化的參數。

 IInfoScreen {
expectedAircraftArrivalUpdate(id: ID, time: Time)
flightCancelled(flightID: ID)
...
}

當然,同時還需要具備定義數據結構的能力。因此,我們添加了這樣的內容:

 long ID
Time {
hour: int
min: int
seconds: int
}

在對接口進行了一段時間的討論之後,我們現在注意到簡單地將接口定義爲一組消息還遠遠不夠。我們希望做到的最小要求是能夠定義消息的方向:它是流入端口還是流出端口?或者更一般地說,系統中存在哪些消息交互模式?我們識別出了好幾個,這裏是onewayrequest-reply的範例:

 IAircraftStatus {
reportPosition(aircraft: ID, pos: Position )
reportProblem {
request (aircraft: ID, problem: Problem, comment: String)
reply (repairProcedure: ID)
}
}

真的是消息嗎?

我們對各種消息交互模式進行了長時間的討論。顯然,消息的其中一種核心用例就是將各種資源的狀態更新發送到各個對其關注的部分。例如,如果航班因爲飛機的一個技術問題而延誤,則該信息就會被髮送到系統的所有InfoScreens中。我們爲一個確切狀態項的“廣播式”完整更新、增量更新、無效更新等方式建立了必需的幾種消息原型。

然而,現實卻給了我們沉重的打擊:我們一直在一種錯誤的抽象中工作!雖然消息傳遞對於這些事項而言是一種適合的傳輸抽象,但我們真正談論的其實應該是複製的數據結構replicated data structures)。基本上,所有的這些結構都採用同樣的方式工作:

  • 定義了一個數據結構(例如FlightInfo)。
  • 系統保持對這樣一組數據結構的跟蹤。
  • 一組數據結構會被幾個組件所更新,而且,通常這組數據結構會被衆多其他的組件所讀取。
  • 從發佈者到接收者的更新策略總是包括對這組數據結構中所有項的完整更新,對一個或多個項的增量更新,無效更新等。

當然,一旦我們瞭解到除了消息之外,系統還包括額外的核心的抽象,我們就應該將它添加到我們的架構語言中,並能夠像下面所示的方式進行編寫。我們定義了數據結構和複製項。然後,組件能夠發佈(publish)或者使用(consume)這些複製的數據結構。

 FlightInfo {
from: Airport
to: Airport
scheduled: Time
expected: Time
...
}

flights {
flights: FlightInfo[]
}

DelayCalculator {
flights
}

InfoScreen {
flights
}

毫無疑問,上面的描述比基於消息的描述更準確。系統能夠自動地衍生出完整更新、增量更新和無效更新等需要的各種消息。這一描述同樣清晰地反映了實際 的架構意圖:比起那種僅僅表達了我們希望如何去做(發送狀態更新消息)的較低級的描述,新的描述方式更好的表達了我們希望做什麼(複製狀態)。

當然,我們還不能停下前進的腳步。現在,我們擁有了作爲“頭等公民”的狀態複製,就能夠爲它的技術規範添加更多的信息:

 DelayCalculator {
flights { = onchange }
}
InfoScreen {
flights { = all = every(60) }
}

上例的意思是,只要底層的數據結構的內容發生改變,發佈者就會發布覆制的數據。然而,InfoScreen只需要每隔60秒進行一次更新(當它剛啓動的時候,會對數據作一次完整的加載)。根據這一信息,我們能夠產生出所有需要的消息,同時爲參與者生成一個更新時間表。

更多內容?

在餘下的討論中,我們識別了架構的其他幾個方面,併爲它們添加了語言抽象:

  • 爲了解決版本衝突,我們增加了一種方法,可以將一個已經存在的組件指定爲按照新版本(替換)方式執行。工具能夠確保“即插即用的兼容性”。
  • 爲了能夠表達消息的語義,以及它們對系統狀態的影響,我們引入了前置條件和後置條件。我們還擴充了組件的概念,將stateful作爲可選項。
  • 最後,我們爲組件添加了可配置參數。組件會指定參數,而組件實例則必須爲它們指定值。

結論

採用這種方法,我們能夠快速地把握系統的整體架構。我們還因此能夠區分開“希望系統做什麼”和“系統如何實現它”:這樣一來,技術層面的討論僅僅屬 於爲此處給出的概念性描述提供實現細節(當然是非常重要的實現細節)。我們明白無誤地理解了不同術語所代表的含義,並給出了明確的定義。組件這一模糊的概 念在這個系統中具有了正式的、明確界定的含義。

當然,它並沒有到此爲止。下一步要討論的是如何爲組件的實現進行編碼,以及討論系統的哪一部份可以被自動生成。更多內容參見下一節。

扼要總結&優勢

我們做了什麼

這種方法包括爲項目或系統的概念性架構定義一門正式的語言。隨着你對架構的深入理解,逐步發展了這門語言。因此,語言總是與你對架構完整而又明晰的理解相對應。隨着我們對語言的增強,我們就能夠使用該語言對應用程序架構進行描述。

背景:DSL

我們前面前面建立起來的語言是一種DSL——領域特定語言。以下是我對DSLs定義:

DSL是一種目的明確的、可處理的語言,當我們在一個特定領域內構建系統時,可以用它來描述一個特定的關注點。它所使用的抽象與標識符號是爲那些指定特定關注點的利益相關人定製的。

DSLs可以用來指定軟件系統的各個方面。其中一大看點是使用DSL可以描述業務功能(例如,在保險系統中的計算規則)。DSL尤其在描述業務功能時倍顯其價值所在,同樣,也完全值得用DSL描述軟件架構:正如我們在這裏所做的那樣。

因此,我們先前構建的架構語言——以及我在本篇論文中倡導的方法——其意義在於使用DSL技術去定義一種描述特定架構的DSL。

優勢

參與的每個人都能清晰地理解用於描述系統的概念。提供清晰明確的詞彙來描述應用程序。創建的模型可以被分析,並作爲代碼生成(如下所示)的基礎被使 用。架構總是與實現細節無關,或者換句話說:概念型架構與技術決策是解耦的,從而使得它們更加便於各自的演化發展。我們同樣能夠根據概念型架構定義一個清 晰的編程模型(如何使用之前定義的所有架構特徵對組件進行建模和編碼)。最後,現在架構師就可以通過構建(或者幫助構建)團隊其餘成員實際能夠使用的工 件,直接爲項目作出貢獻。

爲何使用文本形式?

……或者爲什麼不使用圖形標識?文本型的DSLs有幾大優勢。首先是更加容易建立語言以及一個好的編輯器。其次,文本型的工件比圖形化的模型庫更加 容易集成到現有的開發工具(CVS/SVN diff/merge)中。第三,文本型的DSLs通常更容易被開發者接受,因爲“真正的開發人員不畫圖”。

如果對於系統的某些方面,圖形標識有助於看清楚架構元素之間的關係,你可以使用類似於Graphviz或者Prefuse之類的工具。既然模型以一 種清晰而又幹淨的形式包含了相關的數據,我們就可以輕易的將模型數據導出成GraphViz或者Prefuse工具能夠閱讀的形式。

工具

要使得前面介紹的方法具有可行性,你需要用工具來支持DSLs的高效定義。我們使用了openArchitectureWare的Xtext。Xtext能夠爲你完成如下事情:

  • 它提供了一種定義語法的手段。
  • 根據語法,工具生成一個antlr語法以完成實際的解析。
  • 它同樣會生成以一個EMF Ecore元模型;生成的解析器會實例化從語言的句子中得到的元模型。然後,你能夠使用所有基於EMF的工具去處理這些模型。
  • 你同樣可以根據生成的Ecore模型指定約束。約束可以使用oAW的Check語言(本質上是一個簡化了的OCL)來指定。
  • 最後,工具還可以爲你的DSL生成一個強有力的編輯器,它提供了代碼摺疊、語法着色和可自定義的代碼完成功能,以及一個整體概要視圖和跨文件的轉向定義(go-to-definition)查找引用(find reference)。它還可以實時評估你的語言約束,並輸出錯誤消息。

經過一點實踐就可以掌握Xtext,它真正讓你能夠按照自己對架構細節的理解和架構決策來設計語言。自行訂製代碼完成功能可能需要比較長的時間,但是你可以在對語言的摸索告一段落的時候再做這件事。

驗證模型

如果我們要正式而且準確的描述一個架構,除了語法,我們還需要實施驗證規則,對模型進行約束。簡單的例子比如典型的名稱唯一性約束、類型檢查或非空檢查。要表示這些(相對的)局部約束,可以直接使用OCL或者類似於OCL的語言。

但是,我們還需要驗證更加複雜,而且不那麼局部的約束。例如,在前面介紹的故事裏,約束會檢查組件和接口的新版本是否與它們的舊版本實際上是兼容的,因此可以用在相同的上下文中。要能夠實現這樣重要的約束,有兩個前置條件是非常必要的:

  • 約束自身必須在形式上是可描述的,即必須有某種算法能夠判斷約束是否滿足。一旦你理解了這一算法,就能夠實現它,而不用考慮你的工具支持哪種約束語言(在我們的例子中,約束語言爲類似於OCL的Xtend或者Java)
  • 另一個前置條件是運行前述約束檢測算法所需的數據,要在模型中是實際可用的。例如,如果你想檢驗一個確切的部署方案是否可行,就必須將可用的網絡帶寬、消息的確切時間以及基本日期類型的長度放到模型中6。要捕獲這些數據聽起來是一個負擔,然而,這實際上是一個優勢,因爲這是核心的架構知識。

生成代碼

從本篇論文中可以逐漸清晰地瞭解到,發展架構的DSL(以及使用DSL)的關鍵優勢在於:清晰無誤地理解概念,並正式地定義它們。它有助於你理解你的系統,以及去除那些不必要的技術干擾。

當然,現在我們已經擁有了一個概念型架構的正式模型,以及我們正在構建的系統的正式描述(使用語言定義的語句(或模型)),我們將利用它獲得更多的好處:

  • 我們將爲實現代碼生成API。該API功能強大,考慮了各種消息傳遞範式,複製狀態等等。生成的API允許開發人員用一種不 依賴於任何技術決策的方法對實現進行編碼:生成的API隱藏了組件實現代碼的相關內容。我們將調用這一生成的API,以及用於編程模型的一套術語。
  • 記 住,我們期望通過某種組件容器或中間件平臺運行組件。因此,我們用選定的實現技術生成了運行組件(及組件的技術中立的實現)所必需的代碼。我們將這一層代 碼稱作技術映射代碼(或膠合代碼[glue code])。它通常還會包含各相關平臺的配置文件。有時候,它還需要額外的“混合模型(mix in models)”,爲平臺指定配置細節。生成器將採用開發人員決定使用的技術的最佳實踐。

當然,爲多種目標語言生成API(支持用多種語言來實現組件)以及/或者爲多個目標平臺(支持在不同中間件平臺執行相同的組件)生成膠合代碼都是完 全可行的。這就很好地支持了可能的多平臺的需求,同時也提供了一種方法使得基礎架構能夠隨着時間的推移擴展規模,或者進行演化。

另一個值得注意的是,你通常應該分爲幾個階段來生成代碼:第一個階段是使用類型定義(組件、數據結構、接口)去生成API代碼,這樣你才能對實現進 行編碼。第二個階段是生成膠合代碼以及系統配置代碼。最後,將類型定義從模型中的系統定義分離出來,這是一種明智的做法:因爲在整個過程中,它們會在不同 的時刻被使用,而且通常會被不同的人創建、修改與處理。

總的說來,生成的代碼支持有效的、獨立於技術的實現,能夠隱藏大多數潛在的技術複雜性,從而使得開發更加高效。

如何比較它與ADLs和UML

用正式的語言描述架構並非一個新的想法。各個社區都推薦使用架構描述語言(ADLs)或者統一建模語言(UML)描述架構。有的甚至可以(試圖)從結果模型中生成代碼。但是,所有這些方法都主張使用現有的通用語言來記錄架構(雖然有一些語言能夠被定製化,包括UML)。

然而(你可能從上述的故事中看出端倪)這完全忽略了重點!我並沒有看到這種將架構描述硬塞到預定義/標準化語言提供的(通常是非常有限的)結構中,會帶來多少好處。在本篇論文所闡釋的方法中,其中一個核心活動是實際構建你自己的語言去捕捉系統的概念型架構的過程。讓你的架構適配於ADL或者UML提供的不多的概念,對架構設計並無多大幫助。

關於UML Profile:是的,你可以把前面介紹的方法用在UML上面,建立一個UML Profile,而不是文本型語言。我在好多個項目中採用了這個方法,得到的結論是它在大多數環境下工作得並不好。原因如下:

  • 使用UML需要更多地考慮如何能夠使用UML現有的結構準確地表現你的意圖,無法專注地考慮你的架構概念。這是錯誤的關注方式!
  • 而 且,UML工具通常都無法與你現有的開發基礎架構(編輯器,CVS/SVN,diff/merge)相集成。在某個分析或設計階段,使用UML還不會出現 太大的問題,但是一旦你將你的模型作爲源代碼(它們實際上反映了系統的架構,通過它們生成真正的代碼),就會成爲一個很大的問題。
  • 最後,UML工具通常都是複雜而又重量級的,通常被“真正的”開發人員認爲是“臃腫的軟件”或者“繪圖工具”。使用一門好的文本型語言能夠降低接受的門檻。

爲什麼不直接使用編程語言

架構的抽象,例如消息或組件在現今的第3代編程語言中,並非“頭等公民”。當然,你可以使用類來表示它們。使用註解(也稱爲特性),你甚至可以關聯元數據與類和類的其他內容(操作、字段)。因此,你總是可以使用第三代語言來表示這些內容的。但是,這種方法存在問題:

  • 正如前面闡釋的UML的例子一樣,這種方法會強迫你將清晰的領域特定概念硬塞進預先構建的抽象中。在許多方面,註解/特性與UML的stereotype和tagged value相比,不過是“五十步笑百步”而已,你會遇到相似的問題。
  • 模型的可分析性是有限的。雖然有Spoon for Java這樣的工具能夠對模型進行分析,但是這種分析處理起來並不比一個正式模型更加容易。
  • 最後,用“架構增強型Java或C#”來表達架構,也意味着你試圖混淆架構的關注點與實現的關注點。這會使得這種涇渭分明的區別變得渾濁起來,可能加劇對技術的依賴。

我對組件的觀點

對於什麼是組件存在着許多種(或正式或非正式)定義。從軟件系統的構建模塊,到有顯式定義的上下文依賴關係的物件,到包含了業務邏輯並運行在容器中的物體,都可以稱之爲組件。

我的理解爲(注意,我並不是說我提出了一個真正的定義)組件是最小的架構構建模塊。在定義系統的架構時,無需關注組件的內部。組件必須以聲明方式指 定它們與架構相關的屬性(即以元數據或模型的方式指定)。因此,組件可以通過工具進行分析和組合。通常,它們都運行在容器中,而容器則體現爲框架,處理着 元數據中與運行時相關的部分。容器在哪個層次上提供技術服務(日誌、監控、故障轉移等),那就是組件的邊界。

對於組件實際包含的元數據(以及元數據描述了什麼屬性),我並無任何具體的要求。我認爲,組件的具體概念必須針對每個(系統/平臺/產品類型)架構來定義。而這實際上也是我們在前面介紹的通過語言方式所要做到的。

組件實現

默認情況下,組件的實現都是手動完成的。實現代碼可以針對之前介紹的生成的API代碼來編寫。要想在組件的骨架中增加手工編寫的代碼,開發者可以直接將代碼添加到生成的類中,或者——更好的方式是——使用組合例如繼承或者局部類(partial classes)。

還有其他替代方法可以實現組件,它們不使用第3代編程語言,而是針對需要描述的行爲採用專門的形式化手段。

  • 常規的行爲可用生成器實現。只要先在模型中通過設置少量的定義良好的參數,對其進行參數化之後,即可使用生成器實現。特徵模型(Feature Models)善於表示這種需要進行判斷的多樣性,如此才能夠生成實現的內容。
  • 對於基於狀態的行爲,可以使用狀態機。
  • 對於諸如業務規則的內容,你可以定義一個DSL直接表達這些規則,並使用規則引擎對它們進行運算。現在已經有多個規則引擎可以使用。
  • 對於領域特定的計算,例如在保險領域所常見的情形,你可能需要提供一種專門的表示法來支持領域所需的數學操作。這樣的語言通常是解釋型的:組件在技術上的實現包括一個解釋器,用來對運行的程序進行參數化。

同樣可以使用動作語義語言(ASLs,Action Semantics Languages)作爲替代的方法。但是,需要指明的重要一點是,該語言沒有提供領域特定抽象,而是採用與通用建模語言例如UML相同的方式。然而,即 使你使用了更特定的標記法,仍然免不了需要泛化地指定小個片段的行爲。一個很好的範例就是在狀態機中的動作。

爲了有效地結合用組件概念來定義行爲的各種方法,可以使用元層次的子類化手段去定義各種組件,讓每個組件都有自己的一組表示法去定義行爲。下圖說明了這一原理。

既然從技術上講,組件實現就是與行爲有關,因此通常來說使用封裝在組件內部的解釋器是有效的。

最後,值得一提的是,我們要認識到本節討論的內容只涵蓋應用程序特定的行爲,而不是指所有的實現代碼。大量的實現代碼都與應用程序的技術基礎架構息息相關——遠程處理、持久化、工作流等——而它們都可以從架構模型衍生出來。

模式的角色

在今天的軟件工程實踐中,模式是相當重要的一部分。對於重複出現的問題,模式是一種經過驗證的有效解決方案,模式的適用性、利弊和後果都是經過檢驗的。那麼,模式在前面描述的方法中,又扮演了怎樣的角色呢?

  • 架構模式和模式語言描繪了一些已經被成功運用的架構的藍圖。它們可以啓發你對自己系統架構的構建。一旦你決定使用模式(並且 調整它,使其適用於你的特定上下文),就能夠使得在模式中定義的概念成爲DSL中的“頭等公民”。換句話說,模式影響着架構,因而影響着DSL的語法。
  • 設 計模式,顧名思義,它比架構模式更加具體,更加地貼近特定的實現。設計模式雖然不可能最終成爲架構DSL的中心概念,但是,在通過模型生成代碼時,你的代 碼生成器生成的代碼通常會類似於一些模式的結構。然而需要注意,生成器並不能決定是否應用某個模式:這需要(生成器的)開發人員人工地作出權衡。

在談到DSLs、代碼生成以及模式時,需要提及的是你不能完全地自動化模式!一個模式並不是只包含UML圖表的解決方案。在模式的定義中有很大的篇 幅用來解釋模式受到哪些力量的影響,何時可以應用模式何時不應該應用模式,以及使用模式會帶來何種結果。模式的文檔中通常還會記錄下模式的多種變體,每種 變體都各有不同的優勢與缺點。如果環境有特殊之處,開發人員在實現模式的時候必須將之考慮在內,對它們進行評估,相應作出決策。

哪些內容需要記入文檔?

我一直在鼓吹上述方法可以作爲正式描述系統概念與應用程序架構的一種方法。因此,就意味着它起到了某種文檔的作用,對嗎?

是的,但這並不意味着你不需要將其他任何內容納入文檔。下列內容仍然需要編檔:

  • 基本原理/架構的決策:DSLs描述了架構的輪廓,卻沒有闡釋其原因。你仍然需要對架構 的基本原理與技術決策進行編檔。通常在這裏應該指出相關的(非功能性)需求作爲依據。注意,架構的DSL語法是非常好的着手點。在架構的DSL語法中,每 個結構都源自於大量的架構決策。因此,如果你解釋了每個語法元素爲何能佔據一席之地(以及爲何沒有選擇其他替代物),那麼正好就記錄下了重要的架構決策。 相似的方法也可以用於應用程序的架構,即DSL的實例。
  • 用戶指南:一門語言的語法可以 作爲獲得架構的一種定義良好的正式方法,但是它卻並非一種好的教學工具。因此,你需要爲你的用戶(即應用程序的編程人員)就如何使用架構創建指導文檔。它 包括建模的內容與方式(使用DSL),以及如何生成代碼和如何使用編程模型(如何將實現代碼填充到生成的框架中)。

架構還有許多方面可能值得我們去編檔,但上述兩點是其中最重要的。

進一步閱讀

如果你喜歡本篇論文所闡釋的方法,你可能需要閱讀我整理的架構模式。它們延續了本文的模式話題,併爲本文介紹的內容提供了理論基礎。這篇論文雖然有點舊,但是本質上論述的話題仍然是相同的。可以通過如下地址獲得http://www.voelter.de/data/pub/ArchitecturePatterns.pdf

另外一個值得了解的內容是領域特定語言和模型驅動軟件開發的完整知識。我撰寫了許多關於這方面的文章,最主要的,我還是《模型驅動軟件開發》一書的合作者——從中你可以瞭解到關於技術工程學管理學的內容。更多信息請訪問:http://www.voelter.de/publications/books-mdsd-en.html

當然,通常情況下你還需要了解關於Eclipse建模、openArchitectureWare和Xtext的更多細節內容。在eclipse.org/gmt/oaw上可以訪問到許多相關的信息,包括官方的oAW文檔以及大量的入門視頻。

致謝

我要感謝Iris Groher、Alex Chatziparaskewas、Axel Uhl、Michael Kircher、Tom Quas和Stefan Tilkov,感謝他們對本篇論文的前一個版本提出的精彩評論。

關於作者

Markus Völter是一名獨立的諮詢師,軟件技術和軟件工程的指導教練。他專注於軟件架構、模型驅動軟件開發與領域特定語言以及產品線工程(product line engineering)。Markus就中間件和模型驅動軟件開發領域(合作)撰寫了多篇雜誌文章、書籍以及提出了多種模式。他經常在各種世界大會上發 表演講。你可以給他發送郵件[email protected],或者訪問www.voelter.de與他取得聯繫。

  1. Eric Evans談論的是領域語言,它是一門爲領域和系統的業務功能提供的語言。這當然非常重要,但在本篇論文中,我談論的是爲架構提供的語言。
  2. 不,我不是在談論ADLs或者UML。請接着往下看。
  3. 你還需要藉助某種工具來使用這種語言編寫句子。更多的內容在後面。
  4. 這裏還暗示事實上這種方法尤其適用於大型系統、產品線以及平臺。
  5. 要得出一組具體的組件、界定清楚組件的職責、並進而產生出它們的接口,並非一件輕而易舉的事情。類似CRC卡的技術在這裏非常有用。
  6. 實際上,你可能會將它們放到不同的文件中,因此這個方面不會對核心模型造成污染。但是,這是工具的問題。

查看英文原文:Architecture as Language: A story

 

注:以上內容來自網絡,本人不承擔任何連帶責任

文章轉自:http://www.infoq.com/cn/articles/architecture-as-language-a-story

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章