看過 Martin Fowler 在 InfoQ 的演講影片 Introduction to Domain Specific Languages之後,接下來,選擇看一本 DSL in Action 的書,這本書的內容涵蓋 JVM 所能支援的 DSL,並從各種角度,去分析實現 DSL 方法的優劣。這本書分兩個部份,第一個部份是 1~3 章,再加上 Appendix A,第二個部份則是 DSL 的實作範例,接下來先看第一個部份。
DSL
DSL 的功能是將 problem domain 對應到 solution domain,首先要找出兩個 domain 之間的共通語彙(vacabulary),透過這個共通的專有名詞,來建立 domain expert 跟 IT system 之間的溝通模型。換句話說,就是要從 domain expert 所描述的業務邏輯中,找到該 domain 的專有名詞、專用術語,再以這個專用術語為基礎,建立溝通互動的語言,形成雙方都能使用的 DSL。
Programmer 設計 DSL 時,要注意幾個地方:
要一直把使用者放在心上,因為 DSL 的好壞,不好用的話,會直接影響到使用的意願,那就失去了設計 DSL 的原意,沒有人用的 DSL 就是個失敗的設計。
DSL 只需要針對該業務範圍進行抽象化的設計,沒有多餘的東西,太複雜冗長的語言,只會講低使用者的使用意願。
DSL 分為 Inernal、External、非文本DSL 三種。
DSL 的優缺點
DSL 的優點很容易理解,因為適度的抽象化,可幫助使用者更容易處理複雜的問題,試想,如果沒有 SQL ,那麼我們應該怎麼操作 Relational DB,要直接用 API 下 select() 嗎?
除了優點之外,我們更要注意 DSL 的缺點
- 設計DSL是很困難的工作
- DSL 需要大量前期設計,投入的人力成本
- DSL 增加的中間層,可能會有性能憂慮
- DSL 有時缺少足夠的編輯工具
- 可能會造成「學不完的DSL」現象
- DSL 可能導致語言之間的摩擦:開發APP可能需要同時使用多個 DSL,也因此可能造成整合上的問題
良好的抽象應具有的特質
DSL 牽涉到簡化業務邏輯的設計,這是一種抽象化的過程,我們要知道,要從哪些指標判斷抽象化的優劣
- 極簡:只開放使用者需要使用的功能,沒有多餘、外露的內部實作內。例如 API 要回傳 Map 而不是 TreeMap,可使用繼承的方式,隱藏內部的實作設計。
- 精煉:抽象化的內容不包含任何非本質的細節,移除不必要的細節,可利用 DI(Dependency Injection)隱藏實現的細節
- 擴展性:抽象設計可在不影響現有使用者的情況下,持續改進升級。利用 mixin、functional programming 的 closure、open class 的方式達到擴展性
- 組合性:可與其他抽象設計組合成更高階的抽象設計。使用 Command、Decorator Pattern。但可能會在multithread環境下造成問題。
實作 DSL 的方法
第二章一開始,以一個證券交易的實例設計 DSL,第一個版本是用 Java語法,但遇到交易員不熟悉 Java 語法的問題,而且 Java 語言裡有過多跟證券交易無關的東西。第二個版本是 XML,XML適合描述文件結構,不適合拿來作為 DSL,而且XML有太多無關的標記。因此又有了第三個版本,是使用 Groovy,這個版本才勉強有了個適當的 DSL。
Internal DSL 的分類
生成式:編譯後,轉換生成實作語言的的code
1.1 編譯時meta programming:Lisp, Template Haskell
1.2 執行時meta programming:Ruby, Groovy
內嵌式:領域專用的類別內嵌於宿主語言的類別系統
2.1 Smart API:Java, Ruby
2.2 AST:Java, Ruby, Groovy
2.3 內嵌類別:Haskell, Scala
2.4 反射式meta programming:Ruby, Groovy
External DSL 的分類
上下文驅動的字串操作
xml 轉換成可使用的資源
DSL 工作台
DSL 中內嵌異質代碼
基於解析器組合子的 DSL 設計
實作 DSL 的方法有很多,我們應該如何選擇一個最適當的方法呢?要考慮的因素如下:
- 重用現有的機制:利用強大的宿主語言提供的功能,例如 Scala或 Haskell
- 充分利用現有的知識:要根據開發團隊現有的知識水準來選擇實作的方法。
- 外部DSL的學習曲線:外部DSL可能會很複雜,必須要把學習曲線納入開發成本。
- 適當的表現力:Internal DSL有重用宿主語言的優勢,但相對也約束了描述業務領域的表現力
- 組合性:DSL跟宿主語言之間的是不是能簡單地整合起來
DSL Driven Application Development
在開發 JVM 環境的 DSL 時,最重要的就是要看怎麼跟 JVM 整合在一起,開發的時候,要注意三個問題:1. 整合問題 2. 異常與錯誤的處理 3. 性能的表現。
如果要在一個系統裡面,同時使用多個 DSL,對於 JVM 來說,整合是不成問題的,而且我們可以選擇使用 Java、Groovy、Spring、JRuby、Scala 這些語言來實作 DSL,直接分別將這些實作包裝成 jar,就可以讓主程式引用了。
Internal DSL: Groovy
如果選擇使用了 Groovy,有兩種方式可以將 Groovy Script 整合起來:
使用 javax.script 的 Script Engine,Script Engine 是一種 sandbox,Groovy DSL 跟 Java Class 之間無法互通,另外在出現 Exception 的時候,stack trace 顯示的行號沒辦法直接對應到 DSL 中的行號,不容易除錯。所以在整合 Groovy DSL時,要優先考慮使用第二種方法。
使用 groovy.lang.GroovyClassLoader,以 Groovy 實做的 Order 類別,可直接讓 JVM 使用,在運作 dsl script 之後,也可以直接回傳 Order 的 List,Groovy 跟 Java 之間的互動比 Script Engine 的方法整合地更緊密。
Internal DSL: Spring
Spring 2.0 版之後就支援使用 Ruby、Groovy 實作的 Bean,還能直接運用 Spring 的 DI 功能,動態注入 script code,下面是一個定義 bean 的範例,透過 refresh-check-delay 的設定,spring 將會在每5000毫秒檢查script是否有被更新,而自動 refresh 載入的 bean,系統就可以在不關機的條件下,直接更新業務邏輯。
<lang:jruby
id="accIntCalcRule"
refresh-check-delay="5000"
script-interface="org.spingframework.scripting.AccruedInterestCalculationRule"
script-source="classpath:RubyAccruedInterestCalculationRule.rb">
</lang:jruby>
External DSL: XML
使用 XML Parser 進行文件的解析與處理。
External DSL: ANTLR、JAVACC
ANTLR(Another Tool for Language Recognition)裡面包括了 詞法分析器(Lexer)、語法分析器(Parser)、樹分析器 (tree parser) 的功能,編寫文法(詞法規則與語法規則)描述文件之後,交給ANTLR,就能生成以 Java 語言實作的 parser 程式碼,ANTLR 3.X支援生成 Java,C#,JavaScript,C 這幾種語言的程式碼。
感想
會去看這本 DSL in Action 根本是個意外,原本是要了解 Gradle,然後知道了 Groovy,漸漸地 dig in,最後到了 DSL。讀了幾篇文章後,才了解到,我們在設計系統時,考慮的系統設定模組、外部 API 模組,其實都屬於一種 DSL,我們也可以在設計系統設定時,採用不同於 XML 的方法,設計 API,也可以考慮使用 Groovy實作。而且看了之後,才知道還有太多專有名詞還不了解,現在頂多只能懂得一些皮毛。
以沒寫過 Groovy, Ruby,更別說會寫 Haskell, Scala的狀況,要能深刻了解這本書的內容,還真的有些吃力。接下來,應該把注意力先放回到Groovy。
沒有留言:
張貼留言