簡介:至今, 演化構架和緊急設計 主要關注技術模式的緊急設計,本期將介紹使用特定領域語言 (DSL)捕獲 領域慣用模式。系列作者 Neal Ford 用一個例子說明了該方法,顯示了這種獲取慣用模式 的抽象樣式的優勢。
慣用模式可以是 技術也可以是 領域。技術模式為常用的技術軟件問題指出解決方案,例如在應用程 序(或應用程序套件)中怎樣處理驗證、安全和事務數據。前幾期主要關注獲取技術慣用模式所用的技術 ,例如元程序設計。域模式關注的是如何抽象常見業務問題。而技術模式幾乎出現在所有的軟件中,域模 式之間的差異與業務之間的差異一樣大。然而獲取它們有一套豐富的技術,這就是本期以及後續幾期將要 談論的話題。
本文為使用 DSL 技術作為一種抽象樣式獲取域模式提供動力,DSL 提供多種選擇,包括自己命名的模 式。Martin Fowler 最近的一本書對 DSL 技術有較為深入的研究。在後續幾期中,我會使用他的許多模 式名,也會將他的示例用於我的例子中,逐步講述具體技術。
DSL 的動機
為什麼我們要費 那麼多周折創建一個 DSL ?僅僅為了獲取一個慣用模式?正如我在 “利用可重用代碼,第 2 部分 ” 中指出的,區分慣用模式的最好方法就是讓它看起來與其他代碼不一樣。這種看得見的不同是最 直接的線索,您不需要再看常規 API。同樣,使用 DSL 的目的之一是寫代碼,使這些代碼看起來不像源 代碼而更像您正在嘗試解決的問題。如果能夠達到這個目標(或者接近這個目標),那您將填補軟件項目 中的這一空白,為開發人員和業務涉眾的溝通架起了橋梁。允許用戶閱讀您的代碼很有必要,因為這消除 了將代碼轉換為語言的需求,這是項很容易出錯的工作。讓您的代碼對於非技術人員是易讀的,因為他們 了解軟件的預期設想,這樣你們之間就會有更多的交流。
為激勵開發人員使用這種技術,我將借 用 Fowler DSL 書中的例子。假設我正為一家制作軟件控制的暗格(secret compartments,想想 James Bond)的公司工作。公司的一個客戶,H. 夫人,想要在她的臥室裡裝一個暗格。然而我們公司使用 .com 泡沫破碎後留下的 Java™驅動的 toasters 來運行軟件。盡管 toasters 比較便宜,但更新其中的 軟件卻很昂貴,因此我需要創建基礎暗格代碼,然後將其永久地設置在 toasters 上,然後找到一種方法 根據每位用戶的需求進行配置。您也知道,在現代軟件世界中,這是一個很常見的問題:普遍行為不常改 變,而配置需要根據個人情況進行改變。
H. 夫人想要一個暗格,打開這個暗格的方法是:首先關閉臥室門,接著打開梳妝台的第二個抽屜,最 後打開床頭燈。這些活動必須依次進行,如果打亂次序,必須重頭開始。您可以將控制暗格的軟件想象為 一個狀態機,如圖 1 所示:
圖 1. H. 夫人的暗格是狀態機
基本狀態機 API 比較簡單。創建一個抽象事件類,可以同時處理狀態機內的事件和命令,如清單 1 所示:
清單 1. 狀態機的抽象事件
public class AbstractEvent {
private String name, code;
public AbstractEvent(String name, String code) {
this.name = name;
this.code = code;
}
public String getCode() { return code;}
public String getName() { return name;}
可以使用另一個簡單類 States對狀態機中的狀態建模,如清單 2 所示 :
清單 2. 狀態機類的開始部分
public class States {
private State content;
private List<TransitionBuilder> transitions = new ArrayList<TransitionBuilder>();
private List<Commands> commands = new ArrayList<Commands>();
public States(String name, StateMachineBuilder builder) {
super(name, builder);
content = new State(name);
}
State getState() {
return content;
}
public States actions(Commands... identifiers) {
builder.definingState(this);
commands.addAll(Arrays.asList(identifiers));
return this;
}
public TransitionBuilder transition(Events identifier) {
builder.definingState(this);
return new TransitionBuilder(this, identifier);
}
void addTransition(TransitionBuilder arg) {
transitions.add(arg);
}
void produce() {
for (Commands c : commands)
content.addAction(c.getCommand());
for (TransitionBuilder t : transitions)
t.produce();
}
}
清單 1和 清單 2僅做參考。需要解決的問題是如何表示狀態機的配置。這種表示是安裝暗格的一種慣 用模式。清單 3 展示了狀態機基於 Java 的配置:
清單 3. 一個配置選擇:Java 代碼
Event doorClosed = new Event("doorClosed", "D1CL");
Event drawerOpened = new Event("drawerOpened", "D2OP");
Event lightOn = new Event("lightOn", "L1ON");
Event doorOpened = new Event("doorOpened", "D1OP");
Event panelClosed = new Event("panelClosed", "PNCL");
Command unlockPanelCmd = new Command("unlockPanel", "PNUL");
Command lockPanelCmd = new Command("lockPanel", "PNLK");
Command lockDoorCmd = new Command("lockDoor", "D1LK");
Command unlockDoorCmd = new Command("unlockDoor", "D1UL");
State idle = new State("idle");
State activeState = new State("active");
State waitingForLightState = new State("waitingForLight");
State waitingForDrawerState = new State("waitingForDrawer");
State unlockedPanelState = new State("unlockedPanel");
StateMachine machine = new StateMachine(idle);
idle.addTransition(doorClosed, activeState);
idle.addAction(unlockDoorCmd);
idle.addAction(lockPanelCmd);
activeState.addTransition(drawerOpened, waitingForLightState);
activeState.addTransition(lightOn, waitingForDrawerState);
waitingForLightState.addTransition(lightOn, unlockedPanelState);
waitingForDrawerState.addTransition(drawerOpened, unlockedPanelState);
unlockedPanelState.addAction(unlockPanelCmd);
unlockedPanelState.addAction(lockDoorCmd);
unlockedPanelState.addTransition(panelClosed, idle);
machine.addResetEvents(doorOpened);
清單 3顯示了使用 Java 進行狀態機配置的幾個問題。首先,閱讀這些 Java 代碼並不能明確知道這 就是狀態機配置,和多數 Java API 一樣,這只是一堆沒有差別的代碼。第二,冗長且重復。為狀態機的 每部分設置更多的狀態和轉換時,變量名重復使用,所有這些重復使代碼難於閱讀。第三,代碼不能滿足 最初目標 —— 無需重新編譯就可配置暗格。
事實上,在 Java 世界幾乎看不到這種代碼了,現在流行使用 XML 編寫配置代碼。用 XML 編寫配置 很簡單,如清單 4 所示:
清單 4. 用 XML 編寫的狀態機配置
<stateMachine start = "idle">
<event name="doorClosed" code="D1CL"/>
<event name="drawerOpened" code="D2OP"/>
<event name="lightOn" code="L1ON"/>
<event name="doorOpened" code="D1OP"/>
<event name="panelClosed" code="PNCL"/>
<command name="unlockPanel" code="PNUL"/>
<command name="lockPanel" code="PNLK"/>
<command name="lockDoor" code="D1LK"/>
<command name="unlockDoor" code="D1UL"/>
<state name="idle">
<transition event="doorClosed" target="active"/>
<action command="unlockDoor"/>
<action command="lockPanel"/>
</state>
<state name="active">
<transition event="drawerOpened" target="waitingForLight"/>
<transition event="lightOn" target="waitingForDrawer"/>
</state>
<state name="waitingForLight">
<transition event="lightOn" target="unlockedPanel"/>
</state>
<state name="waitingForDrawer">
<transition event="drawerOpened" target="unlockedPanel"/>
</state>
<state name="unlockedPanel">
<action command="unlockPanel"/>
<action command="lockDoor"/>
<transition event="panelClosed" target="idle"/>
</state>
<resetEvent name = "doorOpened"/>
</stateMachine>
清單 4中的代碼相比 Java 版本有幾個優勢。第一,延遲綁定,這意味著可以修改代碼並將其放進 toaster,可以使用 XML 解析器閱讀配置。第二,對於這個特定問題,這段代碼是更富於表現力,因為 XML 包含容器(containership)概念:States 將它們的配置包含為子元素。這有助於刪除 Java 版本中 令人討厭的冗余。第三,代碼本質上是聲明式的。通常,如果您只是進行聲明而不需要 if和 while語法 ,聲明式代碼更易於閱讀。
暫時退後一步,先理解其含義。外化配置在現代 Java 世界中是一種很常見的模式,我們不再認為它 是獨特實體。實際上這也是每個 Java 框架的特征。配置是一個慣用模式,我們需要捕獲方式,使其區別 於周圍框架的一般行為,並將其分離出來。使用 XML 進行配置,我是使用外部 DSL 編寫代碼的(句法 [syntax] 是 XML,語法 [grammar] 是由 XML 相關模式定義的),因此不需要重新編譯框架代碼對其進 行轉換。
我們沒有必要因為 XML 的優勢,總是使用 XML。可以考慮以下配置代碼,如清單 5 所示:
清單 5. 定制語法(custom-grammar)的狀態機配置
events
doorClosed D1CL
drawerOpened D2OP
lightOn L1ON
doorOpened D1OP
panelClosed PNCL
end
resetEvents
doorOpened
end
commands
unlockPanel PNUL
lockPanel PNLK
lockDoor D1LK
unlockDoor D1UL
end
state idle
actions {unlockDoor lockPanel}
doorClosed => active
end
state active
drawerOpened => waitingForLight
lightOn => waitingForDrawer
end
state waitingForLight
lightOn => unlockedPanel
end
state waitingForDrawer
drawerOpened => unlockedPanel
end
state unlockedPanel
actions {unlockPanel lockDoor}
panelClosed => idle
end
XML 版本有的優勢,它也有:是聲明式的,有容器概念,並且是簡明的。同時它也超越了 XML 和 Java 版本,因為它很少有 噪音字符(例如 <和 >),盡管這對技術實現是必需的,但是影響可讀 性。
此版配置代碼是一個用 ANTLR 編寫的定制外部 DSL,也是一個開源工具,它使得用自定義語言編寫變 得很容易。曾經在大學時候不喜歡編譯器(包括諸如 Lex 和 YACC 之類的經典工具)課程的人,將很高 興知道這些工具已經變得好多了。這個例子來自 Fowler 的書中,他說構建 XML 版本和構建定制語言版 本所用時間相同。
清單 6 中的是用 Ruby 寫的另一種可選版本 :
清單 6. JRuby 中的狀態機配置
event :doorClosed, "D1CL"
event :drawerOpened, "D2OP"
event :lightOn, "L1ON"
event :doorOpened, "D1OP"
event :panelClosed, "PNCL"
command :unlockPanel, "PNUL"
command :lockPanel, "PNLK"
command :lockDoor, "D1LK"
command :unlockDoor, "D1UL"
resetEvents :doorOpened
state :idle do
actions :unlockDoor, :lockPanel
transitions :doorClosed => :active
end
state :active do
transitions :drawerOpened => :waitingForLight,
:lightOn => :waitingForDrawer
end
state :waitingForLight do
transitions :lightOn => :unlockedPanel
end
state :waitingForDrawer do
transitions :drawerOpened => :unlockedPanel
end
state :unlockedPanel do
actions :unlockPanel, :lockDoor
transitions :panelClosed => :idle
end
這是一個很好的 內部DSL 例子:DSL 使用基礎語言的語法,這意味這個 DSL 必須是符合語法的 Ruby 代碼。(因為它是用 Ruby 編寫的,可以使用 JRuby 運行,就是說,您的 toaster 所需的全是 JRuby JAR 文件。)
清單 6同定制語言有許多相同的優點。注意,大量使用 Ruby 塊充當容器,這能給您同 XML 和定制語 言版本一樣的容器語義。它比定制語言使用更少的噪音字符(noise characters)。例如,在 Ruby 中 :前綴表明一個符號,在本例中基本上是用作標識符的不變字符串。
使用 Ruby 實現這類 DSL 相當簡單,如清單 7 所示:
清單 7. JRuby DSL 的部分類定義
class StateMachineBuilder
attr_reader :machine, :events, :states, :commands
def initialize
@events = {}
@states = {}
@state_blocks = {}
@commands = {}
end
def event name, code
@events[name] = Event.new(name.to_s, code)
end
def state name, &block
@states[name] = State.new(name.to_s)
@state_blocks[name] = block
@start_state ||= @states[name]
end
def command name, code
@commands[name] = Command.new(name.to_s, code)
end
Ruby 語法比較靈活,這使它適用於此類 DSL。例如,聲明一個事件時,不會強制包含一個圓括弧作為 方法調用的一部分。在這個版本中,不需要編寫自己的語言或者用尖括弧妨礙自己。這更能說明為什麼這 個方法在 Ruby 世界是如此流行。
DSL 特征
DSL 為捕獲慣用模式提供了很好的可供選擇的語法。正如 Martin Fowler 所定義的 ,DSL 有 5 個主要特征。
計算機編程語言
要成為一個 DSL,這個語言必須是一個計算機編程語言。如果沒有這一限制,容易引起 “滑坡 ”,您遇到的所有事物都有可能是一個 DSL。如果您定義 DSL 術語太廣泛,所有的上下文會話都可 能是 DSL。例如,我有些同事是板球迷,當我同他們在一起時,他們總是不停的談論板球,盡管他們是用 英語,我也不明白他們在說什麼。我缺乏適當的上下文,以至於我不能明白他們所用的單詞。然而,我們 可以使用 DSL 術語談論板球和其他運動。但是如果沒有范圍定義,很難將其縮小到可用約束范圍內 —因此 Fowler 堅持將其限制在計算機編程語言范圍之內。
語言天性
Fowler 關於 DSL 的第二條准則是,它應該有 “語言天性”,這意味您的 DSL 對於非程序員至少是隱約可 讀的。語言天性包含多種格式,在後續幾期中,我將向您展示其中的一些,我將繼續探索 DSL —— 作為一種捕獲慣用模式的方法 —— 的引用。
領域焦點
要成 為一個合適的 DSL,該語言必須只關注一個特定的問題領域,嘗試創建 DSL 的風險之一是使其太寬泛。 DSL 是一個抽象機制,創建太寬泛的抽象會降低它的優勢。
有限的表現力
限制表現力也是 DSL 的一個特點。很少能找到一個 DSL 含有諸如循環和判定的控制結構。DSL 應該特別關注它正在嘗試 描述的領域,而且也只能關注該領域。因此,相當多的 DSL 是聲明式的,而不是指令式的。
非圖 靈完整的(Turing complete)
前面兩個標准暗示了這一特征,但是在這裡,我將正式確認它。您 的 DSL 應當不是圖靈完整的。事實上,人們認為在 DSL 中一個反模式將意外地變成圖靈完整的。例如, 經典的 UNIX® sendmail配置文件就是意外圖靈完整的。您可以在 sendmail配置文件中寫一個操作系 統,如果您願意,而且又有很多時間的話。
意外地變成圖靈完整的是驚人的簡單。一些熟悉的基礎設施工具可以意外地進行這種轉變 —例 如,XSLT。確定一種語言是否是 DSL,有時候取決於其上下文。使用 XSLT 來將一種版本的本文轉換成另 一個版本的文本時,您就是將它作為 DSL 使用的。如果您使用 XSTL 解決漢諾塔問題,您是將它作為一 種圖靈完整語言使用的(並且您可能會找到一種新的愛好)。
結束語
這一期為使用 DSL 作為一種獲取慣用模式的提取機制奠定了基礎。DSL 在這方面做得很不錯,因為它們很容易與常規 API 區分開,更傾向於聲明式的,並改善了項目中開發人員與非開發人員之間的信息交流和反饋。下一期,我 將探索多種構建 DSL 的技術。在後續幾期中,我將逐一介紹幾種可用於尋求發現和設計代碼的 DSL 技術 。