DSL 遷移

2009 年 2 月 4 日

DSL 倡導者需要防範的一個危險觀念是,你首先設計一個 DSL,然後人們使用它。與任何其他軟體裝置一樣,成功的 DSL 會不斷演進。這表示用較早版本的 DSL 編寫的指令碼,在較新版本中執行時可能會失敗。

就像 DSL 的許多特性(好的和壞的)一樣,這與使用函式庫時發生的情況非常類似。如果你從某人那裡取得一個函式庫,然後他們升級了函式庫,你可能會陷入困境。從本質上來說,DSL 並沒有真正做任何事情來改變這一點。你的 DSL 定義基本上是一個 PublishedInterface,你必須以相同的方式處理後果。

這個問題在外部 DSL 中可能會更加明顯。許多對內部 DSL 的變更都可以透過重構工具來處理(對於有這些工具的語言而言)。但是,重構工具無法協助處理外部 DSL。實際上,這個問題並不像想像中那麼嚴重。具有不受 DSL 實作人員控制的指令碼的內部 DSL,不會被重構所採用。因此,內部和外部之間唯一的差別在於同一個程式碼庫中的 DSL 指令碼。

處理 DSL 演進的一種技術是提供自動將 DSL 從一個版本遷移到另一個版本的工具。這些工具可以在升級期間執行,或者在你嘗試對新版本執行舊版本指令碼時自動執行。

有兩種廣泛的方式來處理遷移。第一種是增量遷移策略。這基本上與執行 演化資料庫設計 的人所使用的觀念相同。針對你對 DSL 定義所做的每個變更,建立一個自動將 DSL 指令碼從舊版本遷移到新版本的遷移程式。

增量遷移的一個重要部分是讓變更盡可能小。想像您從版本 1 升級到版本 2,並且想要對 DSL 定義進行十項變更。在此情況下,不要只建立一個遷移指令碼來從版本 1 遷移到版本 2,而是至少建立 10 個指令碼。一次變更一個 DSL 定義功能,並為每個變更撰寫一個遷移指令碼。您可能會發現進一步將其細分並新增一個包含多個步驟(因此有多個遷移)的功能很有用。我所描述的方式聽起來可能比單一指令碼需要更多工作,但重點是如果遷移很小,則會更容易撰寫,而且很容易將多個遷移串連在一起。因此,您撰寫十個指令碼的速度會比撰寫一個指令碼快很多。

另一種方法是基於模型的遷移。如果您使用的是語意模型(這是我幾乎總是建議的),則可以使用此策略。使用此方法,您可以為您的語言支援多個剖析器,每個已發布版本一個。(因此,您只對版本 1 和 2 執行此操作,而不是對中間步驟執行。)每個剖析器都會填入語意模型。當您使用語意模型時,剖析器的行為非常簡單,因此擁有幾個剖析器並不會造成太多麻煩。然後,您可以為您正在使用的指令碼版本執行適當的剖析器。這會處理多個版本,但不會遷移指令碼。若要執行遷移,您可以從語意模型撰寫一個產生器,以產生 DSL 指令碼表示。這樣,您可以為版本 1 指令碼執行剖析器,填入語意模型,然後從產生器發出版本 2 指令碼。

基於模型的方法有一個問題,就是很容易遺失對語意來說不重要的東西,但卻是指令碼撰寫者想要保留的東西。註解是一個明顯的例子。如果剖析器中有太多智慧,這個問題會更加嚴重,但這樣一來,透過這種方式遷移的需要可能會有助於鼓勵剖析器保持單純,這是一件好事。

如果對 DSL 的變更夠大的話,您可能無法將版本 1 範本轉換成版本 2 語意模型。在這種情況下,您可能需要保留版本 1 模型(或中間模型),並讓它具備發出版本 2 範本的能力。

對於這兩個替代方案,我沒有強烈的偏好。

遷移範本可以在需要時由範本程式設計師自行執行,或由 DSL 系統自動執行。為了自動執行,讓範本記錄它是 DSL 的哪個版本非常有用,這樣解析器就能輕鬆地偵測它並觸發結果遷移。