最近領域特定語言(DSLDomain Specific Languages)這個話題比較熱門這可以從Rails現象中看到Rails的流行以及Rails上廣泛使用的領域特定語言(從現在起叫DSL)已經引起了對DSL的廣泛興趣
到現在為止開發人員有這樣的印象建立一個DSL你需要專業的編譯器理論知識理解Lex和Yacc的內部工作原理並需要投入大量的時間來構建DSL結果是極少數人願意去嘗試他們都是從頭開始構建自己的語言
這往往是成本高昂
同時動態語言的愛好者可以毫不費力的利用他們喜歡的動態語言的動態特性來構建領域特定語言事實上他們中的許多以這種方式構建的任何應用程序都有著顯著的復雜性
這兩種方法的差別有重要意義第一種方式是創建屬於自己的語言就是所謂外部的DSL(External DSL)這是一個耗資巨大的項目因為一切都要從頭開始構建需要考慮運算符的優先級規則運行時類庫執行代碼錯誤處理和I/O第二種方法是利用和修改宿主語言就是所謂內部的DSL(Internal DSL)這些都容易構建和維護你只需要考慮如何修改所有的其它東西(通常是你不用關心的)都已經被宿主語言處理了
另一種做法是構建連貫接口(Fluent Interface)把它叫做DSL我認為這不是一種DSL這種方法往往在語言的自由性方面受到很大的限制Java和C#就是很好的例子包括 Java 和C# 你可以列舉許多語言方面的API但這不能讓我覺得這是一個DSL在任何情況下我的個人偏好是使用具有很高語法靈活性的內部DSL因為我基本上都是在CLR上工作我想利用運行在這個平台上的宿主語言它可以讓我重用大部分的使用CLR的知識不要低估這方面的好處在你的手中有一個熟悉的環境是非常重要的
在深入語言之前看看究竟什麼是高語法靈活性的語言怎麼樣?為內部DSL提供一個良好的宿主環境的語言需要具有哪些特性?
我需要有合適的手段來表達我的想法這可以通過有啟發性的命名表達特定域的概念並通常和通用語言的做法不一樣你希望能夠創建一個第四代語言這很容易做到讓我們從一個我們電子表格所使用的腳本這樣簡單的DSL開始如何?
你的任務就是創建乘法網格
for x in range(): for y in range():
cell[ x+ y+ ] = x * y
formula x sum( x x )
這是不是真的令人印象深刻呢?這和編程語言幾乎完全一樣代碼也是微不足道除了和用Excel的自動化API做一樣的工作外更簡短
注意到這就是我們所用到的所有代碼我們不需要一個類的定義或者是一個主方法這是一個沒有任何語法包袱的可執行的DSL腳本
如果前面的例子沒有給你留下深刻的印象看看如何定義訂單折扣的業務規則
apply_discount_of percent:
when orderTotal > and customerIsPreferred
when orderTotal > suggest_registered_to_preferred:
when orderTotal > and not customerIsPreferred
這看起來和編程語言有很大不同它更像業務分析師在Word文檔中定義的業務規則
從我的角度來看上面兩個例子都是領域特定語言他們只是表達領域的方法和風格不同這兩個例子我們實際上已經從語言中移除了和我們的領域沒有直接關系的東西這使得我們可以專注於域並希望有良好的工具來處理
除了域概念以外沒有任何東西可以和具有與域相匹配的語法是一樣重要的
當我們開始在CLR上研究高語法靈活性的語言的時候我們有很多的選擇我們來評估幾個語言我們將從幾個來自微軟的語言開始
C# —— 這是一個非常剛性的語言類型定義沒有獨立的方法/代碼塊僵硬的語言所有這些特性使得C#不是一個DSL宿主語言的好選擇他也可以做到的但它不如其他方法
VBNet —— 其實VBNet更適合面向對象的語言因為它使用了許多英文單詞作為關鍵字和操作符令人遺憾的是它也是一個非常冗長的語言我們要減少冗余性適合我們的域概念
JScript —— 這可能引來一片笑聲但是JScript是一個非常靈活的語言為許多事情提供了較好的語法只要去看看所提供的所有Javascript類庫JScript提供了和Javascript相同的基礎功能雖然這樣有一點不得不考慮的是你可以做到像JQuery或 Prototype那樣多大的靈活性然而它不夠成熟我不確定將來是什麼樣子的雖然它在很多方面有靈活的語法給人有種編程語言的感覺這會讓我在一個DSL中覺得分心
F# —— 這是一門由微軟開發的將來會發布的函數式編程語言F#支持面向對象編程我已經簡略的浏覽過這門語言雖然F#的強大功能令人印象深刻從我的角度來看它看起來是BNF「譯者注BNF BackusNaur Form的縮寫巴科斯范式一種使用形式化符號來描述給定語言的語法」定義其他什麼都不像毫無疑問這是由於作者缺乏函數式編程語言經驗方面的問題但是我不只是考慮它的可讀性
我們已經看完微軟開發的語言讓我們看得更遠些據統計去年CLR上運行的語言超過了一百種所以我選擇了兩種我認為是DSL宿主語言的候選語言
Nemerle是一個多范型的語言(面向對象和函數式)完全支持編譯器宏(後來更多的是Lisp的變種而不是C++)以及許多其他的東西這使得它是一個DSL很好的宿主語言這不是我閱讀Nemerle代碼的簡單理由(經常是這樣)
Boo是一個基於Python語法的靜態類型的面向對象的語言它支持宏(也是Lisp變種)有一個開放的編譯器管道和更容易構建DSL的特性Boo是我首選的用於構建DSLs的語言但是為了保證客觀性我們需要在討論這個主題之前證明Boo有多麼的強大
動態語言運行時(DLR)怎麼樣呢?
到目前為止我還沒有討論動態語言運行時這是一個在CLR之上支持動態語言的微軟項目(目前支持RubyPython和EcmaScript)
更具體的來說當人們討論DLR的時候他們是在討論IronRuby和IronPython Ruby是一門被證明非常適合寫內部DSL的語言在CLR上運行可以使我們在熟悉的環境下工作
使用DLR作為一個DSL的平台當然是可能的但是我至少在一段時間內不使用它DLR和IronRuby本身都還是在開發之中我不認為微軟對發布日期會有任何的承諾此外我沒有發現Ruby能做的Boo做不了我覺得Boo的元編程基礎功能非常自然和強大
自然和非常強大是什麼意思?
讓我們深入一點考查Boo我說它有一個開放的編譯器我並不是指它是開放源代碼的(它是但是和這個無關)我的意思你有辦法深入到編譯器的內部和在編譯的時候打亂編譯器的內部對象模型這意味著我們可以以一種有趣的方式改變編譯器的行為
上面的兩個代碼示例都是Boo的DSL代碼
全面深入Boo的元編程基礎功能超出了本文的范圍但是我可以用一個簡單的例子來展示它的威力
CLR已有IDisposable的概念並配合using語句使用現在我定義一個ITransactionable將用它來定義一個事務的聲明
public interface ITransactionable:
def Dispose():
pass
def Commit():
pass
def Rollback():
passmacro transaction: return [|
tx as ITransactionable = $(transactionArguments[])
try:
$(transactionBody)
txCommit()
except:
txRollback()
raise
finally:
txDispse()
只需要這個代碼我們就可以作為一個頭等語言要素來使用該事務的聲明了(實際上這也正是using語句在Boo的實現方式)
transaction GetNewDatabaseTransaction() DoSomethingWithTheDatabase()
現在如果代碼裡面的事務拋出了一個異常事務將自動回滾如果它執行是成功的事務就自動提交
不過這只是使用Boo的一個示例並注意這裡我介紹的唯一一個概念就是宏和有趣的符號[||]沒有更深入的討論這指示編譯器在事務塊內部的代碼用宏的內容做了一個代碼的替換
很重要的是這已經超越文本替換我們直接修改AST(Abstract Syntax Tree抽象語法樹——編譯器對象模型)這是一個微不足道(但很強大)的例子我們下面將探討一個更復雜的場景這將告訴我們為什麼這樣的區分是重要的
為了構建一個DSL這個級別的功能還是不夠的你可以只使用Boo語言的語法而不使用元編程功能類似於Ruby有很多可選的語法這在很多場景是非常有用的舉個例子來說我們可以不通過元編程創建相同的語法但是利用Boo的這一特性如果方法的最後一個參數是一個委托(閉包塊等等)可以給方法傳遞一個代碼塊
比如
def transaction(tx as ITransactionable transactionalAction as ActionDelegate):
try:
transactionalAction()
txCommit()
except:
txRollback()
raise
finally:
txDispse()
我們仍然可以使用這代碼正如我們前面所用的transaction GetNewDatabaseTransaction() DoSomethingWithTheDatabase()
從語法上來看是沒有差別的 不過這兩個版本還是有微小的差別CLR確保了如果try程序塊能夠成功執行就進入try程序塊執行這是using()語句正確執行的關鍵其他的場景也是一樣的
第一個版本可以利用這個能力第二個版本不能(原因是第二個版本是在運行時調用方法而第一個版本只是替換事務代碼塊得到修改後的結果)
我們還可以利用Boo的元編程做些什麼呢?相當多關於這個內容可以寫一本書(實際上我已經寫了這方面的一本書) )作為一個簡單的例子而不一定是良好設計的的最好例子你可以修改語言的if語句的語義
有一次我不得不這樣做我把if語句修改下面模式if foo == null # do something為這個模式if foo == null or foo isa NullObject # do something現在當我們檢查null的時候我們也檢查這個對象是否是NullObject的實例NullObject是我的應用程序中的一個自定義類型這使得在我的應用程序以一種自然的方式使用NullObject模式
val = NullObject() # set val to a new instance of NullObjectif val == null # will be compiled as val == null or val isa NullObject print Value is nullelse print Value is not null我們已經擴展了語言認為所有繼承自NullObject的對象作為null從長遠來看有能力去修改語言的基本組成部分是我的工作(和語言的使用)容易得多
在繼續下一步之前來看最後一個例子我想告訴你如何在Boo應用程序中使用不到行代碼添加一個(簡單的)契約式設計(類不變式)這是代碼
[AttributeUsage(AttributeTargetsClass)]class EnsureAttribute(AbstractAstAttribute):
expr as Expression def constructor(expr as Expression):
selfexpr = expr def
Apply(target as Node):
type as ClassDefinition = target for member in typeMembers:
method = member as Method continue if method is null
block = methodBody
methodBody = [|
block:
try:
$block
ensure:
assert $expr
|]Block
用法如下
[ensure(name is not null)]class Customer:
name as string def constructor(name as string):
selfname = name def SetName(newName as string):
name = newName
現在任何把名字設置為null都將導致一個斷言異常這個技術相當強大和容易使用我將前置條件的標記的實現留給讀者
這個例子也示范了直接使用編譯器的對象模型(AST)的強大我們不局限於C++宏的文本替換我們可以查詢對象模型並以非常自然的方式修改它那麼現在我想你相信Boo是一個非常適合構建DSL的語言我只是從表面上浏覽了一下它的潛力還有很多有待於你的探索 其他幾項優勢Boo是靜態編譯類型的語言這意味著你的DSL擁有標准CLR代碼的所有優勢(即時編譯器垃圾回收調試等等)從性能角度來看你的應用程序代碼和DSL代碼沒有任何區別
因此基於Boo的DSL對於經常需要修改和需要高性能的代碼是理想的選擇在產品中不得不改變的這樣的共同的需求往往推動人們使用基於XML的系統規則引擎等即使沒有考慮完全用XML編程這樣的辯論這些選擇都遭遇了低性能的問題
建立一個使用一系列DSL腳本的系統很容易從長遠來說是要提供高性能和高度可維護性的系統 他還需要配合領域驅動設計因為有一個領域特定語言更容易表達自然的域概念
有幾個公開的Boo DSL我個人喜歡的是BinsorBinsor是一個Caste Windsor IoC容器的配置DSL使得使用IoC的高級概念易如反掌你需要了解Binsor的更多信息可以通過訪問Binsor 發布說明其它用Boo的DSL是Specter是一個行為驅動開發(BDD)框架提供了一個非常自然的方式書寫規格並將規格轉換為NUnit測試用例
Brail是一個文本模板語言
還有幾個但是使用它的人很少並沒有廣為人知
寫一個DSL要求有一些初步知識但是知識是很簡單和容易獲取的一旦你有了這個基礎知識你就可以開始寫一個DSL就像制作一個表單那麼簡單
實際上我已經寫了一個後端處理系統大部分是從各種來源處理消息的DSL組成的在這個系統中我寫DSL就像在我的表現層上寫表單一樣的
總體來說Boo是一個用於構建DSL非常好的語言使用Boo寫DSL有助於降低成本沒有性能和靈活性方面的妥協此外他還提供了自由的語法和自然的方式表達域的概念
在最後結束時說一句Boo也可以在Java上運行
關於作者Oren Eini也叫Ayende Rahien是一個經驗豐富的NET開發者和架構師他也是好幾個開源項目的貢獻者例如NHibernate和Castle此外Ayende是 Rhino MocksRhino Commons和NHibernate Query Analyzer的創始人關於BooAyende創建了Castle MonoRail的模板語言Brail配置Castle Windsor IoC容器的DSL他還寫了一本標題為《Building Domain Specific Languages in Boo》的書
From:http://tw.wingwit.com/Article/program/net/201311/11679.html