2014/1/5

DSL in Action - written by Debasish Ghosh

看過 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 時,要注意幾個地方:

  1. 要一直把使用者放在心上,因為 DSL 的好壞,不好用的話,會直接影響到使用的意願,那就失去了設計 DSL 的原意,沒有人用的 DSL 就是個失敗的設計。

  2. DSL 只需要針對該業務範圍進行抽象化的設計,沒有多餘的東西,太複雜冗長的語言,只會講低使用者的使用意願。

DSL 分為 Inernal、External、非文本DSL 三種。

DSL 的優缺點

DSL 的優點很容易理解,因為適度的抽象化,可幫助使用者更容易處理複雜的問題,試想,如果沒有 SQL ,那麼我們應該怎麼操作 Relational DB,要直接用 API 下 select() 嗎?

除了優點之外,我們更要注意 DSL 的缺點

  1. 設計DSL是很困難的工作
  2. DSL 需要大量前期設計,投入的人力成本
  3. DSL 增加的中間層,可能會有性能憂慮
  4. DSL 有時缺少足夠的編輯工具
  5. 可能會造成「學不完的DSL」現象
  6. DSL 可能導致語言之間的摩擦:開發APP可能需要同時使用多個 DSL,也因此可能造成整合上的問題

良好的抽象應具有的特質

DSL 牽涉到簡化業務邏輯的設計,這是一種抽象化的過程,我們要知道,要從哪些指標判斷抽象化的優劣

  1. 極簡:只開放使用者需要使用的功能,沒有多餘、外露的內部實作內。例如 API 要回傳 Map 而不是 TreeMap,可使用繼承的方式,隱藏內部的實作設計。
  2. 精煉:抽象化的內容不包含任何非本質的細節,移除不必要的細節,可利用 DI(Dependency Injection)隱藏實現的細節
  3. 擴展性:抽象設計可在不影響現有使用者的情況下,持續改進升級。利用 mixin、functional programming 的 closure、open class 的方式達到擴展性
  4. 組合性:可與其他抽象設計組合成更高階的抽象設計。使用 Command、Decorator Pattern。但可能會在multithread環境下造成問題。

實作 DSL 的方法

第二章一開始,以一個證券交易的實例設計 DSL,第一個版本是用 Java語法,但遇到交易員不熟悉 Java 語法的問題,而且 Java 語言裡有過多跟證券交易無關的東西。第二個版本是 XML,XML適合描述文件結構,不適合拿來作為 DSL,而且XML有太多無關的標記。因此又有了第三個版本,是使用 Groovy,這個版本才勉強有了個適當的 DSL。

Internal DSL 的分類

  1. 生成式:編譯後,轉換生成實作語言的的code

    1.1 編譯時meta programming:Lisp, Template Haskell

    1.2 執行時meta programming:Ruby, Groovy

  2. 內嵌式:領域專用的類別內嵌於宿主語言的類別系統

    2.1 Smart API:Java, Ruby

    2.2 AST:Java, Ruby, Groovy

    2.3 內嵌類別:Haskell, Scala

    2.4 反射式meta programming:Ruby, Groovy

External DSL 的分類

  1. 上下文驅動的字串操作

  2. xml 轉換成可使用的資源

  3. DSL 工作台

  4. DSL 中內嵌異質代碼

  5. 基於解析器組合子的 DSL 設計

實作 DSL 的方法有很多,我們應該如何選擇一個最適當的方法呢?要考慮的因素如下:

  1. 重用現有的機制:利用強大的宿主語言提供的功能,例如 Scala或 Haskell
  2. 充分利用現有的知識:要根據開發團隊現有的知識水準來選擇實作的方法。
  3. 外部DSL的學習曲線:外部DSL可能會很複雜,必須要把學習曲線納入開發成本。
  4. 適當的表現力:Internal DSL有重用宿主語言的優勢,但相對也約束了描述業務領域的表現力
  5. 組合性: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 整合起來:

  1. 使用 javax.script 的 Script Engine,Script Engine 是一種 sandbox,Groovy DSL 跟 Java Class 之間無法互通,另外在出現 Exception 的時候,stack trace 顯示的行號沒辦法直接對應到 DSL 中的行號,不容易除錯。所以在整合 Groovy DSL時,要優先考慮使用第二種方法。

  2. 使用 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。