1.5使用代碼生成
在迄今為止的討論中,要處理DSL,組裝“語義模型”(第11章),然后執行語義模型,提供我們希望從控制器得到的行為。在語言圈子里,這種方式稱為解釋(interpretation)。在解釋文本時,會先解析文本,然后程序立刻產生結果。(在軟件圈子里,解釋是個棘手的詞語,它承載了太多含義,然而,這里嚴格限制為立即執行的形式。)
在語言領域里,與解釋相對的是編譯。在編譯(compilation)時,先解析程序文本,產生中間輸出,然后單獨處理輸出,提供預期行為。在DSL的上下文里,編譯方式通常指的是代碼生成(code generation)。
用狀態機解釋這個差異有點困難,因此,換用另外一個小例子。想象一下,有某種規則判定人們是否符合某種資格,也許是為了滿足保險資格。比如,如圖1-5所示,一個規則是年齡在21~40歲。這個規則可以是一個DSL,檢查像我這樣的候選人是否具備資格。
如果解釋,資格判定處理器會解析規則,在執行時加載語義模型,也許是啟動時加載。當檢查某個候選人時,它會對 這個候選人運行語義模型,獲得一個結果。
如圖1-6所示,在編譯的情況下,解析器會加載語義模型,把它當做資格-判定處理器構建過程的一部分。在構建期間,DSL處理器會產生一些代碼,這些代碼經過編譯、打包,并且納入資格判定處理器,也可能當做某種共享庫。然后,運行這段中間代碼,對候選人進行評估。
例子里的狀態機使用的是解釋:在運行時解析配置代碼,并組成語義模型。但其實也可以生成一些代碼,以免在烤面包機里出現解析器和模型代碼。
代碼生成通常很笨拙,因為它常常需要進行額外的編譯步驟。為了構建程序,首先需要編譯狀態框架和解析器,其次運行解析器,為格蘭特小姐的控制器生成源代碼,然后編譯生成的代碼。這樣做,構建過程就變得復雜許多。
然而,代碼生成的一個優勢在于,編寫解析器和生成代碼可以用不同的語言。在這個情況下,如果生成代碼用的是動態語言,比如JavaScript或是JRuby,第二個編譯步驟就可以省略。
如果所用DSL的語言平臺缺乏支持DSL的工具,代碼生成的作用也會凸顯出來。比如,我們不得不在一些老式的烤面包機上運行這個安全系統,而它們又只能理解編譯過的C,那我們可以這樣做,實現一個代碼生成器,使用組裝的語義模型作為輸入,產生可以編譯為運行在老式烤面包機的C代碼。在最近做的一些項目里,我們曾為MathCAD、SQL和 COBOL等生成代碼。
許多與DSL相關的作品都會關注代碼生成,更有甚者,把代碼生成當做這個活動的主要目標。隨之而來的就是,涌現了一大批文章和書籍,贊美代碼生成的優點。然而,在我看來,代碼生成僅僅是一種實現機制,實際上,大多數情況下 都用不到。當然,有很多情況必須要用代碼生成,但的確有很多情況確實用不到。
許多人用了代碼生成之后,就舍棄了語義模型,他們在解析輸入文本之后,就直接產生生成的代碼。雖然對于使用代碼生成的DSL而言,這也是一種常見的方式,但除非是最簡單的情況,否則不推薦任何人這么做。語義模型的存在,可以將解析、執行語義以及代碼生成分開。整個活動會因為這個劃分變得簡單許多。它也給了我們改變自己想法的機會; 比如,無須修改代碼生成的例程就可以把內部DSL改成外部DSL。類似地,可以很容易產生多種輸出,而無須擔心解析器變得復雜。就同一種語義模型而言,既可以用解釋模型,也可以選擇代碼生成。
因此,在本書的大部分內容里,假設存在一個語義模型,它是DSL工作的核心。
常見的代碼生成風格有兩種。第一種是“第一遍”代碼,這種代碼是一個模板,之后要手動修改。第二種是確保生成代碼絕對不需要手動修改,也許還要排除調試期間所加的追蹤信息。我幾乎總是傾向于后者,因為這樣可以更自由地重 新生成代碼。對DSL而言,這點尤其正確,因為我們希望對于DSL所定義的邏輯而言,它是主要的表現形式。這意味著,無論何時,要修改行為,必須能夠輕松修改DSL。因此,我們必須保證,任何生成的代碼都沒有經過手動編輯,雖然它可以調用手寫的代碼,或者由手寫的代碼調用。