|
英文原文:Architecture as Language: A story
?。ㄗ髡撸篗arkus Völter,譯者:張逸)
2008年4月16日
通常,架構(gòu)要么是在Word文檔中描述的一些軟件系統(tǒng)中無形的、概念性的方面,要么就完全是由技術(shù)驅(qū)動(dòng)的(“我們使用了一個(gè)XML架構(gòu)”)。這兩種方式都很糟糕:前者很難派上用場(chǎng),而后者架構(gòu)上的概念被技術(shù)宣傳所掩蓋。
什么才是好的表達(dá)?應(yīng)該是隨著架構(gòu)的發(fā)展,演化出一門語言,讓你得以從架構(gòu)的角度來描述系統(tǒng)。根據(jù)我在多個(gè)真實(shí)項(xiàng)目中獲得的經(jīng)驗(yàn),這種表達(dá)方式能夠形象、無歧義地描述架構(gòu)構(gòu)建模塊和具體系統(tǒng),同時(shí)又不至于深入到技術(shù)決策的細(xì)節(jié)(技術(shù)決策應(yīng)該有意識(shí)地放到另一個(gè)單獨(dú)的步驟中)。
本篇論文的第一部分通過一個(gè)真實(shí)故事演示了這一思想。第二部分則總結(jié)了這一方法的關(guān)鍵點(diǎn)。
一個(gè)故事
系統(tǒng)背景
我正與一位客戶在一起,他是我負(fù)責(zé)定期咨詢工作中的其中一位客戶。客戶決定構(gòu)建一個(gè)全新的航空管理系統(tǒng)。航空公司使用該系統(tǒng)跟蹤和發(fā)布不同的信息,如:飛機(jī)是否降落在指定的機(jī)場(chǎng);航班是否延遲;飛機(jī)的技術(shù)狀態(tài)等等。系統(tǒng)同時(shí)還要為InterNET的在線跟蹤系統(tǒng)以及在機(jī)場(chǎng)等地設(shè)置的信息監(jiān)控器提供數(shù)據(jù)。無論從哪個(gè)方面來看,該系統(tǒng)都屬于一個(gè)典型的分布式系統(tǒng),系統(tǒng)的各個(gè)部分分別運(yùn)行在不同的機(jī)器上。它有一個(gè)中央數(shù)據(jù)中心負(fù)責(zé)處理繁重的數(shù)字運(yùn)算,還有其他機(jī)器分布放置在相對(duì)廣闊的區(qū)域中。多年來,我的客戶一直在構(gòu)建類似這樣的系統(tǒng),現(xiàn)在他們計(jì)劃引入新一代的系統(tǒng)。新系統(tǒng)必須能夠支持15-20年時(shí)間的演進(jìn)。單單從這一項(xiàng)需求就可以清楚地看出,他們需要對(duì)技術(shù)進(jìn)行某種抽象,因?yàn)樵谶@15-20年期間可能要經(jīng)歷8次技術(shù)潮流的變遷。對(duì)技術(shù)進(jìn)行抽象還有另一個(gè)重要的理由,那就是系統(tǒng)的不同部分采用了不同的技術(shù)來構(gòu)建,有Java,C++,C#。采用多種技術(shù)對(duì)于大型分布式系統(tǒng)而言并非特殊的需求。通常,我們會(huì)在后端使用Java技術(shù),而在Windows前端使用.NET技術(shù)。
由于系統(tǒng)的分布式本質(zhì),不可能在同一時(shí)間更新系統(tǒng)的所有組成部分。這就產(chǎn)生了另一項(xiàng)需求,就是能夠一部分一部分地更新該系統(tǒng)。這就反過來要求能夠管理不同系統(tǒng)組件之間的版本沖突問題(確保組件A在組件B被升級(jí)到一個(gè)新的版本之后,仍然能夠與之協(xié)作)。
起點(diǎn)
在我進(jìn)入項(xiàng)目的時(shí)候,他們已經(jīng)決定系統(tǒng)的主干應(yīng)該是一個(gè)基于消息傳遞的基礎(chǔ)架構(gòu)(對(duì)于這類系統(tǒng)而言,這是一個(gè)不錯(cuò)的決策),并且他們?cè)u(píng)估了不同的消息傳遞主干在性能和吞吐量方面的表現(xiàn)。他們已經(jīng)確定了在整個(gè)系統(tǒng)中使用一個(gè)業(yè)務(wù)對(duì)象模型,對(duì)系統(tǒng)操作的數(shù)據(jù)進(jìn)行描述(對(duì)于這類系統(tǒng)而言,這實(shí)際上不是一個(gè)好的決策,但它不影響這個(gè)故事的結(jié)論)。
因此,當(dāng)我進(jìn)入項(xiàng)目后,他們向我簡(jiǎn)要地介紹了系統(tǒng)的所有細(xì)節(jié),以及他們已經(jīng)做出的架構(gòu)決策,然后詢問我這些決策是否正確。但是我很快就發(fā)現(xiàn),雖然他們了解了很多需求,也已經(jīng)在架構(gòu)的某些方面做出了細(xì)致的決策,但是卻沒有形成我所說的一致的架構(gòu)(consistent architecture):即對(duì)組成實(shí)際系統(tǒng)構(gòu)建模塊的定義,也就是定義系統(tǒng)中的各種事物。他們沒有掌握談?wù)撨@個(gè)系統(tǒng)的語言。
實(shí)際上,這只是我進(jìn)入項(xiàng)目時(shí)的一個(gè)初步印象。當(dāng)然,我認(rèn)為該項(xiàng)目存在一個(gè)巨大的問題:如果你并不知道組成系統(tǒng)的各種事物,就很難一致地談?wù)摵兔枋鲈撓到y(tǒng),當(dāng)然更無法一致地構(gòu)建該系統(tǒng)。你需要定義一門語言。
背景:這門語言是什么?
當(dāng)你擁有一門語言,并能夠從架構(gòu)的角度談?wù)撓到y(tǒng)時(shí),你就擁有了一個(gè)一致的架構(gòu)1。那么語言應(yīng)該是什么樣的呢?顯然,它首先并且至少是一套定義良好的術(shù)語。定義良好首先意味著所有的利益相關(guān)者都要認(rèn)同術(shù)語的含義。如果從非正式的角度來看,術(shù)語和術(shù)語的含義可能就足以定義一門語言了。
然而——這里可能顯得有些突然——我一向鼓吹的是要用一門正式語言來描述架構(gòu)2。要定義一門正式語言,你需要的不僅僅是術(shù)語和術(shù)語的含義。你還需要一種語法來描述如何通過這些術(shù)語組成“語句”(或者模型),同時(shí)需要一種具體的句法去表示它們3。
使用一門正式的語言來描述你的架構(gòu),會(huì)帶來許多好處,隨著故事的逐漸展開,這些好處也會(huì)展露無遺。同時(shí),在本文的末尾我會(huì)對(duì)其進(jìn)行總結(jié)。
發(fā)展出一門語言以描述架構(gòu)
讓我們繼續(xù)這個(gè)故事。我的客戶與我都同意值得花上一天的時(shí)間去審閱某些技術(shù)需求,并為架構(gòu)建立一門正式語言來體現(xiàn)這些需求。實(shí)際上,我們一邊討論整個(gè)架構(gòu),一邊構(gòu)建出語法、某些約束以及一個(gè)編輯器(使用oAW的Xtext工具)。
開始
我們首先從組件的概念開始。我們對(duì)組件概念的定義是相對(duì)比較寬松的。它只是與架構(gòu)相關(guān)的構(gòu)建模塊的最小單元,封裝了應(yīng)用程序的功能。同時(shí),我們假定組件是能夠被實(shí)例化的,以便使架構(gòu)中的組件概念對(duì)應(yīng)上OO編程中的類。因此,根據(jù)我們定義的初始語法,首先構(gòu)建的模型應(yīng)該是這樣:
component DelayCalculator {}component InfoScreen {}
component AircraftModule {}
注意,在這里我們做了兩件事情:我們首先定義了系統(tǒng)中存在組件的概念(使得組件成為我們要構(gòu)建的系統(tǒng)的構(gòu)建模塊),其次我們還(初步)決定系統(tǒng)中存在三個(gè)組件DelayCalculator,InfoScreen和AircraftModule。我們?yōu)榧軜?gòu)提出了一套構(gòu)建模塊,作為一個(gè)概念型的架構(gòu),并將這些構(gòu)建模塊的一套具體范本作為應(yīng)用程序架構(gòu)4。
接口
當(dāng)然,上述關(guān)于組件的概念并無太大用處,因?yàn)榻M件無法交互。領(lǐng)域邏輯清晰地表明,DelayCalculator必須接收來自AircraftModules的消息,從而計(jì)算航班的延誤狀態(tài),然后將結(jié)果轉(zhuǎn)發(fā)給InfoScreens。我們知道,它們應(yīng)該以某種方式交換信息(記住:已經(jīng)作出了消息傳遞決策)。但是,我們決定不引入消息,而是將一組相關(guān)的消息抽象為接口5。
component DelayCalculator implements IDelayCalculator {}component InfoScreen implements IInfoScreen {}
component AircraftModule implements IAircraftModule {}
interface IDelayCalculator {}
interface IInfoScreen {}
interface IAircraftModule {}
我們認(rèn)識(shí)到,上面的代碼看起來有幾分像是Java代碼。無需驚訝,既然我的客戶具有Java背景,那么系統(tǒng)的首選目標(biāo)語言自然就是Java。因此,我們就要從他們習(xí)慣使用的語言中,抽取出廣為人知的概念衍生為我們自己的語言。然而,我們很快注意到這樣的表示方式?jīng)]有太大用處:我們無法表示組件“使用了某個(gè)特定的接口(與提供接口相對(duì))”。了解一個(gè)組件需要哪些接口是很重要的,因?yàn)槲覀兿M軌蛄私猓ǘ抑笠霉ぞ哌M(jìn)行分析)組件具有的依賴關(guān)系。這對(duì)于任何一個(gè)系統(tǒng)都很重要,而對(duì)于版本管理的需求而言,則尤為重要。
因此,我們對(duì)語法稍加修改,支持如下的表達(dá)形式:
component DelayCalculator {provides IDelayCalculator
requires IInfoScreen
}
component InfoScreen {
provides IInfoScreen
}
component AircraftModule {
provides IAircraftModule
requires IDelayCalculator
}
interface IDelayCalculator {}
interface IInfoScreen {}
interface IAircraftModule {}
描述系統(tǒng)
那么,我們來看看這些組件是如何被使用的。我們清晰地認(rèn)識(shí)到組件需要支持實(shí)例化。很顯然,系統(tǒng)中有許多架飛機(jī),每架飛機(jī)都運(yùn)行了一個(gè)AircraftModule組件,而InfoScreens的實(shí)例數(shù)量更多。不夠明確的是我們是否需要多個(gè)DelayCalculators,但我們決定推遲對(duì)它的討論,先處理實(shí)例化的問題。
因此,我們需要能夠表示組件的實(shí)例化。
instance screen1: InfoScreeninstance screen2: InfoScreen
...
接著,我們討論了如何把系統(tǒng)的各實(shí)例“接上線”:如何表示某個(gè)InfoScreen與某個(gè)DelayCalculator“交談”?我們必須找出某種方式來表示實(shí)例之間的關(guān)系。由于這兩個(gè)類型各自具有了“可兼容”的接口,因此,DelayCalculator可以與InfoScreen“交談”。但是暫時(shí)還難以把握這種“交談”關(guān)系。我們還注意到一個(gè)DelayCalculator實(shí)例通常會(huì)與多個(gè)InfoScreen實(shí)例“對(duì)話”。因此,我們必須以某種方式在語言中引入下標(biāo)來表示實(shí)例的個(gè)數(shù)。
經(jīng)過幾番修改,我引入了端口(Port)的概念(實(shí)際上在組件技術(shù)以及UML中,這是一個(gè)眾所周知的概念,但是相對(duì)于我的客戶而言,卻是一個(gè)新名詞)。端口是在組件類型上定義的一個(gè)通信端點(diǎn),當(dāng)擁有端口的組件被實(shí)例化時(shí),端口也會(huì)一同被實(shí)例化。因此,我們對(duì)組件描述語言進(jìn)行重構(gòu),以支持如下的表示形式。端口通過provides和requires關(guān)鍵字進(jìn)行定義,緊接著是端口的名稱和下標(biāo),一個(gè)冒號(hào)以及與端口相關(guān)聯(lián)的接口。
component DelayCalculator {provides default: IDelayCalculator
requires screens[0..n]: IInfoScreen
}
component InfoScreen {
provides default: IInfoScreen
}
component AircraftModule {
provides default: IAircraftModule
requires calculator[1]: IDelayCalculator
}
以上模型表示,任何一個(gè)DelayCalculator實(shí)例都要連接多個(gè)InfoScreens。從DelayCalculator實(shí)現(xiàn)代碼的角度來看,通過screen端口可以訪問到一組InfoScreen。而AircraftModule則只能與一個(gè)DelayCalculator“對(duì)話”,正如下標(biāo)[1]所示。
新的接口標(biāo)識(shí)啟發(fā)了我的客戶對(duì)IDelayCalculator進(jìn)行了修改,因?yàn)樗麄冏⒁獾綄?duì)于不同的通信對(duì)象,應(yīng)該有不同的接口(因此還應(yīng)該有不同的端口)。我們對(duì)應(yīng)用程序架構(gòu)作出了如下修改:
component DelayCalculator {provides aircraft: IAircraftStatus
provides managementConsole: IManagementConsole
requires screens[0..n]: IInfoScreen
}
component Manager {
requires backend[1]: IManagementConsole
}
component InfoScreen {
provides default: IInfoScreen
}
component AircraftModule {
requires calculator[1]: IAircraftStatus
}
注意,端口的引入改善了應(yīng)用程序架構(gòu),因?yàn)槲覀儞碛辛梭w現(xiàn)角色的接口(IAircraftStatus,IManagementConsole)。
現(xiàn)在,我們擁有了端口,因此我們能夠命名通信端點(diǎn)。這就使得我們能夠輕而易舉地描繪出系統(tǒng):互連的組件實(shí)例。注意,引入了新的結(jié)構(gòu)connect。
instance dc: DelayCalculatorinstance screen1: InfoScreen
instance screen2: InfoScreen
connect dc.screens to (screen1.default, screen2.default)
保持大局觀
當(dāng)然,從某種情況來看,為了不至于混淆所有的組件、實(shí)例和連接器(connectors),我們無疑需要引入某種命名空間的概念。自然,我們也可以將這些內(nèi)容分別放到不同的文件中(工具支持保證了“轉(zhuǎn)到定義”和“查找引用”仍然正常)。
namespace com.mycompany {namespace datacenter {
component DelayCalculator {
provides aircraft: IAircraftStatus
provides managementConsole: IManagementConsole
requires screens[0..n]: IInfoScreen
}
component Manager {
requires backend[1]: IManagementConsole
}
}
namespace mobile {
component InfoScreen {
provides default: IInfoScreen
}
component AircraftModule {
requires calculator[1]: IAircraftStatus
}
}
}
當(dāng)然,將組件和接口的定義(本質(zhì)上是類型的定義)與系統(tǒng)的定義(連接的實(shí)例)分開,是一個(gè)很好的想法,因次,我們?nèi)缦露x了一個(gè)系統(tǒng):
namespace com.mycompany.test {system testSystem {
instance dc: DelayCalculator
instance screen1: InfoScreen
instance screen2: InfoScreen
connect dc.screens to (screen1.default, screen2.default)
}
}
在一個(gè)真實(shí)的系統(tǒng)中,DelayCalculator必須能夠在運(yùn)行時(shí)動(dòng)態(tài)地發(fā)現(xiàn)所有可用的InfoScreens。手動(dòng)地描述這些連接是沒有什么意義的。因此,我們需要繼續(xù)前進(jìn)。我們定義了一個(gè)查詢,它可以采用naming/trader/lookup/registry的基礎(chǔ)架構(gòu)在運(yùn)行時(shí)執(zhí)行。每隔60秒,查詢會(huì)被執(zhí)行一次,查找任何上線的InfoScreens。
namespace com.mycompany.production {instance dc: DelayCalculator
// InfoScreen instances are created and
// started in other configurations
dynamic connect dc.screens every 60 query {
type = IInfoScreen
status = active
}
}
可以使用相似的辦法實(shí)現(xiàn)負(fù)載均衡或者容錯(cuò)能力。一個(gè)靜態(tài)的連接器能夠指向一個(gè)主要實(shí)例以及備份實(shí)例?;蛘撸诋?dāng)前使用的組件實(shí)例變?yōu)椴豢捎脮r(shí),可以重新執(zhí)行一個(gè)動(dòng)態(tài)查詢。
為了支持實(shí)例的注冊(cè),我們?cè)谒鼈兊亩x中添加了額外的語法。一個(gè)registered的實(shí)例會(huì)在注冊(cè)記錄中使用自己的名稱(通過命名空間識(shí)別)以及所有提供的接口,自動(dòng)注冊(cè)其本身。還可以指定額外的參數(shù),如下的例子就為DelayCalculator注冊(cè)了一個(gè)主要的實(shí)例和一個(gè)備份的實(shí)例。
namespace com.mycompany.datacenter {registered instance dc1: DelayCalculator {
registration parameters {role = primary}
}
registered instance dc2: DelayCalculator {
registration parameters {role = backup}
}
}
第二部分,接口
至今我們?nèi)匀粵]有真正定義一個(gè)接口究竟是什么。我們知道,我們更愿意基于一個(gè)消息傳遞的基礎(chǔ)架構(gòu)來構(gòu)建系統(tǒng),因此,接口顯然必須定義為消息的集合。于是就有了我們最初的想法:一組消息的集合,其中每條消息都有名稱,以及一組類型化的參數(shù)。
interface IInfoScreen {message expectedAircraftArrivalUpdate(id: ID, time: Time)
message flightCancelled(flightID: ID)
...
}
當(dāng)然,同時(shí)還需要具備定義數(shù)據(jù)結(jié)構(gòu)的能力。因此,我們添加了這樣的內(nèi)容:
typedef long IDstruct Time {
hour: int
min: int
seconds: int
}
在對(duì)接口進(jìn)行了一段時(shí)間的討論之后,我們現(xiàn)在注意到簡(jiǎn)單地將接口定義為一組消息還遠(yuǎn)遠(yuǎn)不夠。我們希望做到的最小要求是能夠定義消息的方向:它是流入端口還是流出端口?或者更一般地說,系統(tǒng)中存在哪些消息交互模式?我們識(shí)別出了好幾個(gè),這里是oneway和request-reply的范例:
interface IAircraftStatus {oneway message reportPosition(aircraft: ID, pos: Position )
request-reply message reportProblem {
request (aircraft: ID, problem: Problem, comment: String)
reply (repairProcedure: ID)
}
}
真的是消息嗎?
我們對(duì)各種消息交互模式進(jìn)行了長(zhǎng)時(shí)間的討論。顯然,消息的其中一種核心用例就是將各種資源的狀態(tài)更新發(fā)送到各個(gè)對(duì)其關(guān)注的部分。例如,如果航班因?yàn)轱w機(jī)的一個(gè)技術(shù)問題而延誤,則該信息就會(huì)被發(fā)送到系統(tǒng)的所有InfoScreens中。我們?yōu)橐粋€(gè)確切狀態(tài)項(xiàng)的“廣播式”完整更新、增量更新、無效更新等方式建立了必需的幾種消息原型。
然而,現(xiàn)實(shí)卻給了我們沉重的打擊:我們一直在一種錯(cuò)誤的抽象中工作!雖然消息傳遞對(duì)于這些事項(xiàng)而言是一種適合的傳輸抽象,但我們真正談?wù)摰钠鋵?shí)應(yīng)該是復(fù)制的數(shù)據(jù)結(jié)構(gòu)(replicated data structures)。基本上,所有的這些結(jié)構(gòu)都采用同樣的方式工作:
- 定義了一個(gè)數(shù)據(jù)結(jié)構(gòu)(例如FlightInfo)。
- 系統(tǒng)保持對(duì)這樣一組數(shù)據(jù)結(jié)構(gòu)的跟蹤。
- 一組數(shù)據(jù)結(jié)構(gòu)會(huì)被幾個(gè)組件所更新,而且,通常這組數(shù)據(jù)結(jié)構(gòu)會(huì)被眾多其他的組件所讀取。
- 從發(fā)布者到接收者的更新策略總是包括對(duì)這組數(shù)據(jù)結(jié)構(gòu)中所有項(xiàng)的完整更新,對(duì)一個(gè)或多個(gè)項(xiàng)的增量更新,無效更新等。
當(dāng)然,一旦我們了解到除了消息之外,系統(tǒng)還包括額外的核心的抽象,我們就應(yīng)該將它添加到我們的架構(gòu)語言中,并能夠像下面所示的方式進(jìn)行編寫。我們定義了數(shù)據(jù)結(jié)構(gòu)和復(fù)制項(xiàng)。然后,組件能夠發(fā)布(publish)或者使用(consume)這些復(fù)制的數(shù)據(jù)結(jié)構(gòu)。
struct FlightInfo {from: Airport
to: Airport
scheduled: Time
expected: Time
...
}
replicated singleton flights {
flights: FlightInfo[]
}
component DelayCalculator {
publishes flights
}
component InfoScreen {
consumes flights
}
毫無疑問,上面的描述比基于消息的描述更準(zhǔn)確。系統(tǒng)能夠自動(dòng)地衍生出完整更新、增量更新和無效更新等需要的各種消息。這一描述同樣清晰地反映了實(shí)際的架構(gòu)意圖:比起那種僅僅表達(dá)了我們希望如何去做(發(fā)送狀態(tài)更新消息)的較低級(jí)的描述,新的描述方式更好的表達(dá)了我們希望做什么(復(fù)制狀態(tài))。
當(dāng)然,我們還不能停下前進(jìn)的腳步?,F(xiàn)在,我們擁有了作為“頭等公民”的狀態(tài)復(fù)制,就能夠?yàn)樗募夹g(shù)規(guī)范添加更多的信息:
component DelayCalculator {publishes flights { publication = onchange }
}
component InfoScreen {
consumes flights { init = all update = every(60) }
}
上例的意思是,只要底層的數(shù)據(jù)結(jié)構(gòu)的內(nèi)容發(fā)生改變,發(fā)布者就會(huì)發(fā)布復(fù)制的數(shù)據(jù)。然而,InfoScreen只需要每隔60秒進(jìn)行一次更新(當(dāng)它剛啟動(dòng)的時(shí)候,會(huì)對(duì)數(shù)據(jù)作一次完整的加載)。根據(jù)這一信息,我們能夠產(chǎn)生出所有需要的消息,同時(shí)為參與者生成一個(gè)更新時(shí)間表。
更多內(nèi)容?
在余下的討論中,我們識(shí)別了架構(gòu)的其他幾個(gè)方面,并為它們添加了語言抽象:
- 為了解決版本沖突,我們?cè)黾恿艘环N方法,可以將一個(gè)已經(jīng)存在的組件指定為按照新版本(替換)方式執(zhí)行。工具能夠確保“即插即用的兼容性”。
- 為了能夠表達(dá)消息的語義,以及它們對(duì)系統(tǒng)狀態(tài)的影響,我們引入了前置條件和后置條件。我們還擴(kuò)充了組件的概念,將stateful作為可選項(xiàng)。
- 最后,我們?yōu)榻M件添加了可配置參數(shù)。組件會(huì)指定參數(shù),而組件實(shí)例則必須為它們指定值。
結(jié)論
采用這種方法,我們能夠快速地把握系統(tǒng)的整體架構(gòu)。我們還因此能夠區(qū)分開“希望系統(tǒng)做什么”和“系統(tǒng)如何實(shí)現(xiàn)它”:這樣一來,技術(shù)層面的討論僅僅屬于為此處給出的概念性描述提供實(shí)現(xiàn)細(xì)節(jié)(當(dāng)然是非常重要的實(shí)現(xiàn)細(xì)節(jié))。我們明白無誤地理解了不同術(shù)語所代表的含義,并給出了明確的定義。組件這一模糊的概念在這個(gè)系統(tǒng)中具有了正式的、明確界定的含義。
當(dāng)然,它并沒有到此為止。下一步要討論的是如何為組件的實(shí)現(xiàn)進(jìn)行編碼,以及討論系統(tǒng)的哪一部份可以被自動(dòng)生成。更多內(nèi)容參見下一節(jié)。
扼要總結(jié)&優(yōu)勢(shì)
我們做了什么
這種方法包括為項(xiàng)目或系統(tǒng)的概念性架構(gòu)定義一門正式的語言。隨著你對(duì)架構(gòu)的深入理解,逐步發(fā)展了這門語言。因此,語言總是與你對(duì)架構(gòu)完整而又明晰的理解相對(duì)應(yīng)。隨著我們對(duì)語言的增強(qiáng),我們就能夠使用該語言對(duì)應(yīng)用程序架構(gòu)進(jìn)行描述。
背景:DSL
我們前面前面建立起來的語言是一種DSL——領(lǐng)域特定語言。以下是我對(duì)DSLs定義:
DSL是一種目的明確的、可處理的語言,當(dāng)我們?cè)谝粋€(gè)特定領(lǐng)域內(nèi)構(gòu)建系統(tǒng)時(shí),可以用它來描述一個(gè)特定的關(guān)注點(diǎn)。它所使用的抽象與標(biāo)識(shí)符號(hào)是為那些指定特定關(guān)注點(diǎn)的利益相關(guān)人定制的。
DSLs可以用來指定軟件系統(tǒng)的各個(gè)方面。其中一大看點(diǎn)是使用DSL可以描述業(yè)務(wù)功能(例如,在保險(xiǎn)系統(tǒng)中的計(jì)算規(guī)則)。DSL尤其在描述業(yè)務(wù)功能時(shí)倍顯其價(jià)值所在,同樣,也完全值得用DSL描述軟件架構(gòu):正如我們?cè)谶@里所做的那樣。
因此,我們先前構(gòu)建的架構(gòu)語言——以及我在本篇論文中倡導(dǎo)的方法——其意義在于使用DSL技術(shù)去定義一種描述特定架構(gòu)的DSL。
優(yōu)勢(shì)
參與的每個(gè)人都能清晰地理解用于描述系統(tǒng)的概念。提供清晰明確的詞匯來描述應(yīng)用程序。創(chuàng)建的模型可以被分析,并作為代碼生成(如下所示)的基礎(chǔ)被使用。架構(gòu)總是與實(shí)現(xiàn)細(xì)節(jié)無關(guān),或者換句話說:概念型架構(gòu)與技術(shù)決策是解耦的,從而使得它們更加便于各自的演化發(fā)展。我們同樣能夠根據(jù)概念型架構(gòu)定義一個(gè)清晰的編程模型(如何使用之前定義的所有架構(gòu)特征對(duì)組件進(jìn)行建模和編碼)。最后,現(xiàn)在架構(gòu)師就可以通過構(gòu)建(或者幫助構(gòu)建)團(tuán)隊(duì)其余成員實(shí)際能夠使用的工件,直接為項(xiàng)目作出貢獻(xiàn)。
為何使用文本形式?
……或者為什么不使用圖形標(biāo)識(shí)?文本型的DSLs有幾大優(yōu)勢(shì)。首先是更加容易建立語言以及一個(gè)好的編輯器。其次,文本型的工件比圖形化的模型庫更加容易集成到現(xiàn)有的開發(fā)工具(CVS/SVN diff/merge)中。第三,文本型的DSLs通常更容易被開發(fā)者接受,因?yàn)?ldquo;真正的開發(fā)人員不畫圖”。
如果對(duì)于系統(tǒng)的某些方面,圖形標(biāo)識(shí)有助于看清楚架構(gòu)元素之間的關(guān)系,你可以使用類似于Graphviz或者Prefuse之類的工具。既然模型以一種清晰而又干凈的形式包含了相關(guān)的數(shù)據(jù),我們就可以輕易的將模型數(shù)據(jù)導(dǎo)出成GraphViz或者Prefuse工具能夠閱讀的形式。
工具
要使得前面介紹的方法具有可行性,你需要用工具來支持DSLs的高效定義。我們使用了openArchitectureWare的Xtext。Xtext能夠?yàn)槟阃瓿扇缦率虑椋?/p>
- 它提供了一種定義語法的手段。
- 根據(jù)語法,工具生成一個(gè)antlr語法以完成實(shí)際的解析。
- 它同樣會(huì)生成以一個(gè)EMF Ecore元模型;生成的解析器會(huì)實(shí)例化從語言的句子中得到的元模型。然后,你能夠使用所有基于EMF的工具去處理這些模型。
- 你同樣可以根據(jù)生成的Ecore模型指定約束。約束可以使用oAW的Check語言(本質(zhì)上是一個(gè)簡(jiǎn)化了的OCL)來指定。
- 最后,工具還可以為你的DSL生成一個(gè)強(qiáng)有力的編輯器,它提供了代碼折疊、語法著色和可自定義的代碼完成功能,以及一個(gè)整體概要視圖和跨文件的轉(zhuǎn)向定義(go-to-definition)和查找引用(find reference)。它還可以實(shí)時(shí)評(píng)估你的語言約束,并輸出錯(cuò)誤消息。
經(jīng)過一點(diǎn)實(shí)踐就可以掌握Xtext,它真正讓你能夠按照自己對(duì)架構(gòu)細(xì)節(jié)的理解和架構(gòu)決策來設(shè)計(jì)語言。自行訂制代碼完成功能可能需要比較長(zhǎng)的時(shí)間,但是你可以在對(duì)語言的摸索告一段落的時(shí)候再做這件事。
驗(yàn)證模型
如果我們要正式而且準(zhǔn)確的描述一個(gè)架構(gòu),除了語法,我們還需要實(shí)施驗(yàn)證規(guī)則,對(duì)模型進(jìn)行約束。簡(jiǎn)單的例子比如典型的名稱唯一性約束、類型檢查或非空檢查。要表示這些(相對(duì)的)局部約束,可以直接使用OCL或者類似于OCL的語言。
但是,我們還需要驗(yàn)證更加復(fù)雜,而且不那么局部的約束。例如,在前面介紹的故事里,約束會(huì)檢查組件和接口的新版本是否與它們的舊版本實(shí)際上是兼容的,因此可以用在相同的上下文中。要能夠?qū)崿F(xiàn)這樣重要的約束,有兩個(gè)前置條件是非常必要的:
- 約束自身必須在形式上是可描述的,即必須有某種算法能夠判斷約束是否滿足。一旦你理解了這一算法,就能夠?qū)崿F(xiàn)它,而不用考慮你的工具支持哪種約束語言(在我們的例子中,約束語言為類似于OCL的Xtend或者Java)
- 另一個(gè)前置條件是運(yùn)行前述約束檢測(cè)算法所需的數(shù)據(jù),要在模型中是實(shí)際可用的。例如,如果你想檢驗(yàn)一個(gè)確切的部署方案是否可行,就必須將可用的網(wǎng)絡(luò)帶寬、消息的確切時(shí)間以及基本日期類型的長(zhǎng)度放到模型中6。要捕獲這些數(shù)據(jù)聽起來是一個(gè)負(fù)擔(dān),然而,這實(shí)際上是一個(gè)優(yōu)勢(shì),因?yàn)檫@是核心的架構(gòu)知識(shí)。
生成代碼
從本篇論文中可以逐漸清晰地了解到,發(fā)展架構(gòu)的DSL(以及使用DSL)的關(guān)鍵優(yōu)勢(shì)在于:清晰無誤地理解概念,并正式地定義它們。它有助于你理解你的系統(tǒng),以及去除那些不必要的技術(shù)干擾。
當(dāng)然,現(xiàn)在我們已經(jīng)擁有了一個(gè)概念型架構(gòu)的正式模型,以及我們正在構(gòu)建的系統(tǒng)的正式描述(使用語言定義的語句(或模型)),我們將利用它獲得更多的好處:
- 我們將為實(shí)現(xiàn)代碼生成API。該API功能強(qiáng)大,考慮了各種消息傳遞范式,復(fù)制狀態(tài)等等。生成的API允許開發(fā)人員用一種不依賴于任何技術(shù)決策的方法對(duì)實(shí)現(xiàn)進(jìn)行編碼:生成的API隱藏了組件實(shí)現(xiàn)代碼的相關(guān)內(nèi)容。我們將調(diào)用這一生成的API,以及用于編程模型的一套術(shù)語。
- 記住,我們期望通過某種組件容器或中間件平臺(tái)運(yùn)行組件。因此,我們用選定的實(shí)現(xiàn)技術(shù)生成了運(yùn)行組件(及組件的技術(shù)中立的實(shí)現(xiàn))所必需的代碼。我們將這一層代碼稱作技術(shù)映射代碼(或膠合代碼[glue code])。它通常還會(huì)包含各相關(guān)平臺(tái)的配置文件。有時(shí)候,它還需要額外的“混合模型(mix in models)”,為平臺(tái)指定配置細(xì)節(jié)。生成器將采用開發(fā)人員決定使用的技術(shù)的最佳實(shí)踐。
當(dāng)然,為多種目標(biāo)語言生成API(支持用多種語言來實(shí)現(xiàn)組件)以及/或者為多個(gè)目標(biāo)平臺(tái)(支持在不同中間件平臺(tái)執(zhí)行相同的組件)生成膠合代碼都是完全可行的。這就很好地支持了可能的多平臺(tái)的需求,同時(shí)也提供了一種方法使得基礎(chǔ)架構(gòu)能夠隨著時(shí)間的推移擴(kuò)展規(guī)模,或者進(jìn)行演化。
另一個(gè)值得注意的是,你通常應(yīng)該分為幾個(gè)階段來生成代碼:第一個(gè)階段是使用類型定義(組件、數(shù)據(jù)結(jié)構(gòu)、接口)去生成API代碼,這樣你才能對(duì)實(shí)現(xiàn)進(jìn)行編碼。第二個(gè)階段是生成膠合代碼以及系統(tǒng)配置代碼。最后,將類型定義從模型中的系統(tǒng)定義分離出來,這是一種明智的做法:因?yàn)樵谡麄€(gè)過程中,它們會(huì)在不同的時(shí)刻被使用,而且通常會(huì)被不同的人創(chuàng)建、修改與處理。
總的說來,生成的代碼支持有效的、獨(dú)立于技術(shù)的實(shí)現(xiàn),能夠隱藏大多數(shù)潛在的技術(shù)復(fù)雜性,從而使得開發(fā)更加高效。
如何比較它與ADLs和UML
用正式的語言描述架構(gòu)并非一個(gè)新的想法。各個(gè)社區(qū)都推薦使用架構(gòu)描述語言(ADLs)或者統(tǒng)一建模語言(UML)描述架構(gòu)。有的甚至可以(試圖)從結(jié)果模型中生成代碼。但是,所有這些方法都主張使用現(xiàn)有的通用語言來記錄架構(gòu)(雖然有一些語言能夠被定制化,包括UML)。
然而(你可能從上述的故事中看出端倪)這完全忽略了重點(diǎn)!我并沒有看到這種將架構(gòu)描述硬塞到預(yù)定義/標(biāo)準(zhǔn)化語言提供的(通常是非常有限的)結(jié)構(gòu)中,會(huì)帶來多少好處。在本篇論文所闡釋的方法中,其中一個(gè)核心活動(dòng)是實(shí)際構(gòu)建你自己的語言去捕捉系統(tǒng)的概念型架構(gòu)的過程。讓你的架構(gòu)適配于ADL或者UML提供的不多的概念,對(duì)架構(gòu)設(shè)計(jì)并無多大幫助。
關(guān)于UML Profile:是的,你可以把前面介紹的方法用在UML上面,建立一個(gè)UML Profile,而不是文本型語言。我在好多個(gè)項(xiàng)目中采用了這個(gè)方法,得到的結(jié)論是它在大多數(shù)環(huán)境下工作得并不好。原因如下:
- 使用UML需要更多地考慮如何能夠使用UML現(xiàn)有的結(jié)構(gòu)準(zhǔn)確地表現(xiàn)你的意圖,無法專注地考慮你的架構(gòu)概念。這是錯(cuò)誤的關(guān)注方式!
- 而且,UML工具通常都無法與你現(xiàn)有的開發(fā)基礎(chǔ)架構(gòu)(編輯器,CVS/SVN,diff/merge)相集成。在某個(gè)分析或設(shè)計(jì)階段,使用UML還不會(huì)出現(xiàn)太大的問題,但是一旦你將你的模型作為源代碼(它們實(shí)際上反映了系統(tǒng)的架構(gòu),通過它們生成真正的代碼),就會(huì)成為一個(gè)很大的問題。
- 最后,UML工具通常都是復(fù)雜而又重量級(jí)的,通常被“真正的”開發(fā)人員認(rèn)為是“臃腫的軟件”或者“繪圖工具”。使用一門好的文本型語言能夠降低接受的門檻。
為什么不直接使用編程語言
架構(gòu)的抽象,例如消息或組件在現(xiàn)今的第3代編程語言中,并非“頭等公民”。當(dāng)然,你可以使用類來表示它們。使用注解(也稱為特性),你甚至可以關(guān)聯(lián)元數(shù)據(jù)與類和類的其他內(nèi)容(操作、字段)。因此,你總是可以使用第三代語言來表示這些內(nèi)容的。但是,這種方法存在問題:
- 正如前面闡釋的UML的例子一樣,這種方法會(huì)強(qiáng)迫你將清晰的領(lǐng)域特定概念硬塞進(jìn)預(yù)先構(gòu)建的抽象中。在許多方面,注解/特性與UML的stereotype和tagged value相比,不過是“五十步笑百步”而已,你會(huì)遇到相似的問題。
- 模型的可分析性是有限的。雖然有Spoon for Java這樣的工具能夠?qū)δP瓦M(jìn)行分析,但是這種分析處理起來并不比一個(gè)正式模型更加容易。
- 最后,用“架構(gòu)增強(qiáng)型Java或C#”來表達(dá)架構(gòu),也意味著你試圖混淆架構(gòu)的關(guān)注點(diǎn)與實(shí)現(xiàn)的關(guān)注點(diǎn)。這會(huì)使得這種涇渭分明的區(qū)別變得渾濁起來,可能加劇對(duì)技術(shù)的依賴。
我對(duì)組件的觀點(diǎn)
對(duì)于什么是組件存在著許多種(或正式或非正式)定義。從軟件系統(tǒng)的構(gòu)建模塊,到有顯式定義的上下文依賴關(guān)系的物件,到包含了業(yè)務(wù)邏輯并運(yùn)行在容器中的物體,都可以稱之為組件。
我的理解為(注意,我并不是說我提出了一個(gè)真正的定義)組件是最小的架構(gòu)構(gòu)建模塊。在定義系統(tǒng)的架構(gòu)時(shí),無需關(guān)注組件的內(nèi)部。組件必須以聲明方式指定它們與架構(gòu)相關(guān)的屬性(即以元數(shù)據(jù)或模型的方式指定)。因此,組件可以通過工具進(jìn)行分析和組合。通常,它們都運(yùn)行在容器中,而容器則體現(xiàn)為框架,處理著元數(shù)據(jù)中與運(yùn)行時(shí)相關(guān)的部分。容器在哪個(gè)層次上提供技術(shù)服務(wù)(日志、監(jiān)控、故障轉(zhuǎn)移等),那就是組件的邊界。
對(duì)于組件實(shí)際包含的元數(shù)據(jù)(以及元數(shù)據(jù)描述了什么屬性),我并無任何具體的要求。我認(rèn)為,組件的具體概念必須針對(duì)每個(gè)(系統(tǒng)/平臺(tái)/產(chǎn)品類型)架構(gòu)來定義。而這實(shí)際上也是我們?cè)谇懊娼榻B的通過語言方式所要做到的。
組件實(shí)現(xiàn)
默認(rèn)情況下,組件的實(shí)現(xiàn)都是手動(dòng)完成的。實(shí)現(xiàn)代碼可以針對(duì)之前介紹的生成的API代碼來編寫。要想在組件的骨架中增加手工編寫的代碼,開發(fā)者可以直接將代碼添加到生成的類中,或者——更好的方式是——使用組合例如繼承或者局部類(partial classes)。
還有其他替代方法可以實(shí)現(xiàn)組件,它們不使用第3代編程語言,而是針對(duì)需要描述的行為采用專門的形式化手段。
- 常規(guī)的行為可用生成器實(shí)現(xiàn)。只要先在模型中通過設(shè)置少量的定義良好的參數(shù),對(duì)其進(jìn)行參數(shù)化之后,即可使用生成器實(shí)現(xiàn)。特征模型(Feature Models)善于表示這種需要進(jìn)行判斷的多樣性,如此才能夠生成實(shí)現(xiàn)的內(nèi)容。
- 對(duì)于基于狀態(tài)的行為,可以使用狀態(tài)機(jī)。
- 對(duì)于諸如業(yè)務(wù)規(guī)則的內(nèi)容,你可以定義一個(gè)DSL直接表達(dá)這些規(guī)則,并使用規(guī)則引擎對(duì)它們進(jìn)行運(yùn)算?,F(xiàn)在已經(jīng)有多個(gè)規(guī)則引擎可以使用。
- 對(duì)于領(lǐng)域特定的計(jì)算,例如在保險(xiǎn)領(lǐng)域所常見的情形,你可能需要提供一種專門的表示法來支持領(lǐng)域所需的數(shù)學(xué)操作。這樣的語言通常是解釋型的:組件在技術(shù)上的實(shí)現(xiàn)包括一個(gè)解釋器,用來對(duì)運(yùn)行的程序進(jìn)行參數(shù)化。
同樣可以使用動(dòng)作語義語言(ASLs,Action Semantics Languages)作為替代的方法。但是,需要指明的重要一點(diǎn)是,該語言沒有提供領(lǐng)域特定抽象,而是采用與通用建模語言例如UML相同的方式。然而,即使你使用了更特定的標(biāo)記法,仍然免不了需要泛化地指定小個(gè)片段的行為。一個(gè)很好的范例就是在狀態(tài)機(jī)中的動(dòng)作。
為了有效地結(jié)合用組件概念來定義行為的各種方法,可以使用元層次的子類化手段去定義各種組件,讓每個(gè)組件都有自己的一組表示法去定義行為。下圖說明了這一原理。
既然從技術(shù)上講,組件實(shí)現(xiàn)就是與行為有關(guān),因此通常來說使用封裝在組件內(nèi)部的解釋器是有效的。
最后,值得一提的是,我們要認(rèn)識(shí)到本節(jié)討論的內(nèi)容只涵蓋應(yīng)用程序特定的行為,而不是指所有的實(shí)現(xiàn)代碼。大量的實(shí)現(xiàn)代碼都與應(yīng)用程序的技術(shù)基礎(chǔ)架構(gòu)息息相關(guān)——遠(yuǎn)程處理、持久化、工作流等——而它們都可以從架構(gòu)模型衍生出來。
模式的角色
在今天的軟件工程實(shí)踐中,模式是相當(dāng)重要的一部分。對(duì)于重復(fù)出現(xiàn)的問題,模式是一種經(jīng)過驗(yàn)證的有效解決方案,模式的適用性、利弊和后果都是經(jīng)過檢驗(yàn)的。那么,模式在前面描述的方法中,又扮演了怎樣的角色呢?
- 架構(gòu)模式和模式語言描繪了一些已經(jīng)被成功運(yùn)用的架構(gòu)的藍(lán)圖。它們可以啟發(fā)你對(duì)自己系統(tǒng)架構(gòu)的構(gòu)建。一旦你決定使用模式(并且調(diào)整它,使其適用于你的特定上下文),就能夠使得在模式中定義的概念成為DSL中的“頭等公民”。換句話說,模式影響著架構(gòu),因而影響著DSL的語法。
- 設(shè)計(jì)模式,顧名思義,它比架構(gòu)模式更加具體,更加地貼近特定的實(shí)現(xiàn)。設(shè)計(jì)模式雖然不可能最終成為架構(gòu)DSL的中心概念,但是,在通過模型生成代碼時(shí),你的代碼生成器生成的代碼通常會(huì)類似于一些模式的結(jié)構(gòu)。然而需要注意,生成器并不能決定是否應(yīng)用某個(gè)模式:這需要(生成器的)開發(fā)人員人工地作出權(quán)衡。
在談到DSLs、代碼生成以及模式時(shí),需要提及的是你不能完全地自動(dòng)化模式!一個(gè)模式并不是只包含UML圖表的解決方案。在模式的定義中有很大的篇幅用來解釋模式受到哪些力量的影響,何時(shí)可以應(yīng)用模式何時(shí)不應(yīng)該應(yīng)用模式,以及使用模式會(huì)帶來何種結(jié)果。模式的文檔中通常還會(huì)記錄下模式的多種變體,每種變體都各有不同的優(yōu)勢(shì)與缺點(diǎn)。如果環(huán)境有特殊之處,開發(fā)人員在實(shí)現(xiàn)模式的時(shí)候必須將之考慮在內(nèi),對(duì)它們進(jìn)行評(píng)估,相應(yīng)作出決策。
哪些內(nèi)容需要記入文檔?
我一直在鼓吹上述方法可以作為正式描述系統(tǒng)概念與應(yīng)用程序架構(gòu)的一種方法。因此,就意味著它起到了某種文檔的作用,對(duì)嗎?
是的,但這并不意味著你不需要將其他任何內(nèi)容納入文檔。下列內(nèi)容仍然需要編檔:
- 基本原理/架構(gòu)的決策:DSLs描述了架構(gòu)的輪廓,卻沒有闡釋其原因。你仍然需要對(duì)架構(gòu)的基本原理與技術(shù)決策進(jìn)行編檔。通常在這里應(yīng)該指出相關(guān)的(非功能性)需求作為依據(jù)。注意,架構(gòu)的DSL語法是非常好的著手點(diǎn)。在架構(gòu)的DSL語法中,每個(gè)結(jié)構(gòu)都源自于大量的架構(gòu)決策。因此,如果你解釋了每個(gè)語法元素為何能占據(jù)一席之地(以及為何沒有選擇其他替代物),那么正好就記錄下了重要的架構(gòu)決策。相似的方法也可以用于應(yīng)用程序的架構(gòu),即DSL的實(shí)例。
- 用戶指南:一門語言的語法可以作為獲得架構(gòu)的一種定義良好的正式方法,但是它卻并非一種好的教學(xué)工具。因此,你需要為你的用戶(即應(yīng)用程序的編程人員)就如何使用架構(gòu)創(chuàng)建指導(dǎo)文檔。它包括建模的內(nèi)容與方式(使用DSL),以及如何生成代碼和如何使用編程模型(如何將實(shí)現(xiàn)代碼填充到生成的框架中)。
架構(gòu)還有許多方面可能值得我們?nèi)ゾ帣n,但上述兩點(diǎn)是其中最重要的。
進(jìn)一步閱讀
如果你喜歡本篇論文所闡釋的方法,你可能需要閱讀我整理的架構(gòu)模式。它們延續(xù)了本文的模式話題,并為本文介紹的內(nèi)容提供了理論基礎(chǔ)。這篇論文雖然有點(diǎn)舊,但是本質(zhì)上論述的話題仍然是相同的。可以通過如下地址獲得http://www.voelter.de/data/pub/ArchitecturePatterns.pdf。
另外一個(gè)值得了解的內(nèi)容是領(lǐng)域特定語言和模型驅(qū)動(dòng)軟件開發(fā)的完整知識(shí)。我撰寫了許多關(guān)于這方面的文章,最主要的,我還是《模型驅(qū)動(dòng)軟件開發(fā)》一書的合作者——從中你可以了解到關(guān)于技術(shù)、工程學(xué)、管理學(xué)的內(nèi)容。更多信息請(qǐng)?jiān)L問:http://www.voelter.de/publications/books-mdsd-en.html。
當(dāng)然,通常情況下你還需要了解關(guān)于Eclipse建模、openArchitectureWare和Xtext的更多細(xì)節(jié)內(nèi)容。在eclipse.org/gmt/oaw上可以訪問到許多相關(guān)的信息,包括官方的oAW文檔以及大量的入門視頻。
致謝
我要感謝Iris Groher、Alex Chatziparaskewas、Axel Uhl、Michael Kircher、Tom Quas和Stefan Tilkov,感謝他們對(duì)本篇論文的前一個(gè)版本提出的精彩評(píng)論。
關(guān)于作者
Markus Völter是一名獨(dú)立的咨詢師,軟件技術(shù)和軟件工程的指導(dǎo)教練。他專注于軟件架構(gòu)、模型驅(qū)動(dòng)軟件開發(fā)與領(lǐng)域特定語言以及產(chǎn)品線工程(product line engineering)。Markus就中間件和模型驅(qū)動(dòng)軟件開發(fā)領(lǐng)域(合作)撰寫了多篇雜志文章、書籍以及提出了多種模式。他經(jīng)常在各種世界大會(huì)上發(fā)表演講。你可以給他發(fā)送郵件voelter@acm.org,或者訪問www.voelter.de與他取得聯(lián)系。
- Eric Evans談?wù)摰氖穷I(lǐng)域語言,它是一門為領(lǐng)域和系統(tǒng)的業(yè)務(wù)功能提供的語言。這當(dāng)然非常重要,但在本篇論文中,我談?wù)摰氖菫榧軜?gòu)提供的語言。
- 不,我不是在談?wù)揂DLs或者UML。請(qǐng)接著往下看。
- 你還需要借助某種工具來使用這種語言編寫句子。更多的內(nèi)容在后面。
- 這里還暗示事實(shí)上這種方法尤其適用于大型系統(tǒng)、產(chǎn)品線以及平臺(tái)。
- 要得出一組具體的組件、界定清楚組件的職責(zé)、并進(jìn)而產(chǎn)生出它們的接口,并非一件輕而易舉的事情。類似CRC卡的技術(shù)在這里非常有用。
- 實(shí)際上,你可能會(huì)將它們放到不同的文件中,因此這個(gè)方面不會(huì)對(duì)核心模型造成污染。但是,這是工具的問題。
it知識(shí)庫:將架構(gòu)作為語言:一個(gè)故事,轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。