如何設(shè)計(jì)領(lǐng)域特定語(yǔ)言,實(shí)現(xiàn)終極業(yè)務(wù)抽象?
在過去的幾年里,我一直從事于各種領(lǐng)域定義語(yǔ)言的設(shè)計(jì),包含 unflow、guarding、datum、forming 等。在我剛?cè)腴T這個(gè)領(lǐng)域的時(shí)候,我從《領(lǐng)域特定語(yǔ)言》、《編程語(yǔ)言實(shí)現(xiàn)模式》 等,一直研究到龍書等。我漸漸掌握了領(lǐng)域特定語(yǔ)言設(shè)計(jì)的一些技巧,也能快速(相對(duì)于過去)設(shè)計(jì)出一個(gè)領(lǐng)域特定語(yǔ)言。
所以,我在想我應(yīng)該總結(jié)一下相關(guān)的套路。這樣一來,也可以在未來驗(yàn)證現(xiàn)在的思路是否正確:
- 定義呈現(xiàn)模式。
- 提煉領(lǐng)域特定名詞。
- 設(shè)計(jì)關(guān)聯(lián)關(guān)系與語(yǔ)法。
- 實(shí)現(xiàn)語(yǔ)法解析。
- 演進(jìn)語(yǔ)言的設(shè)計(jì)。
領(lǐng)域特定語(yǔ)言
領(lǐng)域特定語(yǔ)言(英語(yǔ):domain-specific language、DSL)指的是專注于某個(gè)應(yīng)用程序領(lǐng)域的計(jì)算機(jī)語(yǔ)言。
本文所寫的皆是外部 DSL,即『不同于應(yīng)用系統(tǒng)主要使用語(yǔ)言』的語(yǔ)言。創(chuàng)建外部 DSL 和創(chuàng)建一種通用目的的編程語(yǔ)言的過程是相似的,它可以是編譯型或者解釋型的。
通用目的編程語(yǔ)言的源代碼和外部 DSL 的源代碼之間的主要區(qū)別在于,經(jīng)過編譯的 DSL 通常不會(huì)直接產(chǎn)生可執(zhí)行的程序(但是它確實(shí)可以)。大多數(shù)情況下,外部 DSL 可以轉(zhuǎn)換為一種與核心應(yīng)用程序的操作環(huán)境相兼容的資源,也可以轉(zhuǎn)換為用于構(gòu)建核心應(yīng)用的通用目的編程語(yǔ)言。—— Vaughn Vernon
簡(jiǎn)單場(chǎng)景下的領(lǐng)域特定語(yǔ)言,只是將特定的源碼轉(zhuǎn)換為特定的數(shù)據(jù)結(jié)構(gòu)。如 JSON 便是一種 DSL,在 Java 語(yǔ)言里,需要將它轉(zhuǎn)換為對(duì)應(yīng)的數(shù)據(jù)類。復(fù)雜場(chǎng)景下的領(lǐng)域特定語(yǔ)言,可以直接編譯為可執(zhí)行程序。
外部 DSL 的麻煩點(diǎn)在于:
- 語(yǔ)法設(shè)計(jì)
- 語(yǔ)法解析
- IDE 支持
當(dāng)然了,它的優(yōu)點(diǎn)也很明顯:
- 讓不懂編程的業(yè)務(wù)專家(領(lǐng)域?qū)<?快速編寫核心邏輯。
- 領(lǐng)域邏輯與具體編程語(yǔ)言無關(guān)
- 平臺(tái)無關(guān)。
更多的信息,建議去閱讀《領(lǐng)域特定語(yǔ)言》一書。
定義呈現(xiàn)模式
領(lǐng)域特定語(yǔ)言嘛,從需求上就是對(duì)于業(yè)務(wù)呈現(xiàn)的簡(jiǎn)化。根據(jù)不同的呈現(xiàn)模式,去解析源碼,得到我們所需要的數(shù)據(jù)結(jié)構(gòu)。
呈現(xiàn)模式
如下是常見的的領(lǐng)域特定語(yǔ)言的使用模式 [wiki_dsl]:
- 獨(dú)立的工具,如 Makefile
- 在編譯時(shí)或?qū)崟r(shí)轉(zhuǎn)換為宿主語(yǔ)言
- 嵌入式領(lǐng)域特定語(yǔ)言
- ……
可以參見維基百科,我就不再去翻譯了。
[wikidsl]: https://en.wikipedia.org/wiki/Domain-specificlanguage
定義數(shù)據(jù)結(jié)構(gòu)
從通用語(yǔ)言的編譯過程來看:
- 詞法分析器,分析輸入的字符流,得到符號(hào)流。
- 語(yǔ)法分析,分析符號(hào)流,得到語(yǔ)法樹
- 語(yǔ)義分析,分析語(yǔ)法樹,得到新的語(yǔ)法樹
- 中間代碼生成器,分析語(yǔ)法樹,得到中間表示形式
- ……
步驟 1~4,對(duì)于通用語(yǔ)言和領(lǐng)域特定語(yǔ)言來說都是極為類似的。唯一擁有區(qū)別的是這個(gè)中間表示形式,對(duì)于領(lǐng)域特定語(yǔ)言來說,我們場(chǎng)景的原因,這里往往是我們所需要的數(shù)據(jù)結(jié)構(gòu)。
當(dāng)然了,從某種意義上來說,AST(抽象語(yǔ)法樹)也是一種數(shù)據(jù)結(jié)構(gòu),只不過它是一種中間的數(shù)據(jù)結(jié)構(gòu)。所以,有時(shí)候在設(shè)計(jì)的時(shí)候,我就偷懶直接輸出中間表示了。
提煉領(lǐng)域特定名詞
這個(gè)環(huán)節(jié)的過程,實(shí)現(xiàn)上和 DDD(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì))里的提煉問題域以獲取領(lǐng)域知識(shí)是頗為相似的。同樣的這個(gè)過程中,通過與領(lǐng)域?qū)<业膮f(xié)作,我們才能獲得更好的領(lǐng)域特定語(yǔ)言。
從用例開始
用例,或譯使用案例、用況,是軟件工程或系統(tǒng)工程中對(duì)系統(tǒng)如何反應(yīng)外界請(qǐng)求的描述,是一種通過用戶的使用場(chǎng)景來獲取需求的技術(shù)。
在進(jìn)行領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)協(xié)作時(shí),我們需要與領(lǐng)域?qū)<依斫庥脩粼谶@個(gè)過程中,進(jìn)行的一系列操作,以提煉我們所需要的統(tǒng)一語(yǔ)言。而其中的用例能描述達(dá)到目標(biāo)所需的步驟,包含用戶和系統(tǒng)之間的交互。
在創(chuàng)建領(lǐng)域特定語(yǔ)言的時(shí)候,這個(gè)過程對(duì)于我們來說,也是類似的:與領(lǐng)域?qū)<乙黄饏f(xié)作,從用例開始提煉。它也可以直接由現(xiàn)有的代碼中提煉而來。
從已有用例入手
對(duì)于已有系統(tǒng)來說,用例可以由:
與領(lǐng)域?qū)<医涣鳙@取。與領(lǐng)域?qū)<伊奶?,是我們獲得用例的最好方式。記錄用例,從而獲得關(guān)鍵信息。
從現(xiàn)有的代碼中提取。
在 ArchUnit 中提取架構(gòu)規(guī)劃上的設(shè)計(jì)便是:
- classes().that().resideInAPackage("..foo..")
- .should().onlyHaveDependentClassesThat().resideInAnyPackage("..source.one..", "..f
對(duì)應(yīng)的,我們?cè)?Guarding 的設(shè)計(jì)是:
- class(resideIn "..foo..") dependent package(resideIn ["..source.one..", "..foo.."]
在 Guarding 中設(shè)計(jì)的是針對(duì)主流的編程語(yǔ)言,所以在語(yǔ)法上會(huì)盡量與編程語(yǔ)言無關(guān)。
提取關(guān)鍵字、值、屬性
在獲得了用例作為輸入條件之后,我們就需要從中提取一些關(guān)鍵信息,如關(guān)鍵字、值、屬性等等。
如下是我在設(shè)計(jì) Guarding DSL 時(shí),從 ArchUnit 提取的一小部分關(guān)鍵信息:
- package:
- dependOn
- class:
- implement
- annotation
- annotatedWith
- expression:
- and
- or
- not
- equals
- only
接著,我們就可以依據(jù)這些信息,展開它們的關(guān)聯(lián)設(shè)計(jì),進(jìn)而設(shè)計(jì)我們的語(yǔ)法。
設(shè)計(jì)關(guān)聯(lián)關(guān)系與語(yǔ)法
在設(shè)計(jì)領(lǐng)域特定語(yǔ)言時(shí),我們主要以實(shí)現(xiàn)領(lǐng)域中的用例作為目標(biāo):
- 使用 DSL 描述一個(gè)用例
- 先不考慮語(yǔ)法實(shí)現(xiàn),實(shí)現(xiàn)大部分用例的 DSL 草稿版本
- 對(duì)齊不同用例 DSL 中的差異
- 考慮一些非常規(guī)的用例,添加額外的屬性
名詞關(guān)系與邏輯設(shè)計(jì)
領(lǐng)域特定語(yǔ)言,所針對(duì)的是特定領(lǐng)域。在特定的領(lǐng)域里,都會(huì)使用特定的詞匯來描述相關(guān)之間的關(guān)系。這個(gè)關(guān)系,便是我們?cè)O(shè)計(jì)語(yǔ)法的一個(gè)關(guān)鍵。
如在 Java 語(yǔ)言里,使用: implement、 extends 來表示兩個(gè)類之間的關(guān)系。而為了表示包之間的關(guān)系,則會(huì)有: dependent、 resideIn 等等的關(guān)系。
實(shí)現(xiàn)用例
實(shí)現(xiàn)用例并不是一個(gè)復(fù)雜的過程,只是要符合人類的思維習(xí)慣,并盡可能地簡(jiǎn)化設(shè)計(jì)。不過,覺得注意的是,我們應(yīng)該留下一些證據(jù)來告訴未來的自己:我們當(dāng)時(shí)是為什么考慮的。
在設(shè)計(jì) DSL 時(shí),我往往會(huì)創(chuàng)建一個(gè) sample 文件,以記錄過程中,對(duì)于不同的要素的思索。如我在設(shè)計(jì) Guarding DSL 里,使用了一個(gè) 0.0.1.sample 文本文件,來描述早期版本的語(yǔ)法示例:
- # 正則表達(dá)式
- package(match("^/app")) endsWith "Connection";
- package("..home..")::name should not contains(matching(""));
- # 簡(jiǎn)化的比較
- class::name.len should < 20;
通過一些注釋來讓自己優(yōu)化設(shè)計(jì)。
實(shí)現(xiàn)語(yǔ)法解析
這一部分的過程,和我們學(xué)習(xí)編譯原理時(shí)基本是一致的。不過呢,在編寫領(lǐng)域特定語(yǔ)言的時(shí)候,我們一般會(huì)使用解析器生成器,而不是手寫解析器。
細(xì)節(jié)設(shè)計(jì)
設(shè)計(jì)領(lǐng)域特定語(yǔ)言的時(shí)候,在設(shè)計(jì)語(yǔ)法上的拘束不會(huì)像通用語(yǔ)言那么多。所以,自由設(shè)計(jì)的范圍就大一點(diǎn),有些內(nèi)容也不一定需要像編程語(yǔ)言麻煩。諸如于:
- 分隔符
- 縮進(jìn)的處理
- 語(yǔ)法塊的開始和結(jié)束
- ……
PS:使用類似于編程語(yǔ)言的寫法,對(duì)于寫 DSL 的非編程人士來說可能會(huì)變成一種困擾。
解析器生成器
經(jīng)典的 Lex & Yacc 是你可以考慮的范圍,在不同的語(yǔ)言里也有一些相似的實(shí)現(xiàn)。
對(duì)于我來說,以下是我常用的一些解析器生成器。
- Antlr。支持主流的語(yǔ)言
- Peg.js。JavaScript
- Pest。Rust
- Lalrpop。Rust
我還是比較習(xí)慣用 Antlr,支持的語(yǔ)言較多。我與同事以及開源社區(qū)的小伙伴們,在下面的項(xiàng)目中都使用過 Antlr:
- Coca = Golang + Antlr
- Unflow = Rust + Antlr
- Lemonj = JavaScript/TypeScript + Antlr
- Chapi = Java/Kotlin + Antlr
從使用上它們之間的差距并不大,但是都需要學(xué)習(xí)成本。
演進(jìn)語(yǔ)言的設(shè)計(jì)
最后,讓我們來談?wù)勔恍┯幸馑嫉臇|西,雖說是演進(jìn)吧,但是,和設(shè)計(jì)暫時(shí)沒有太大的關(guān)系。
測(cè)試驅(qū)動(dòng)開發(fā)
經(jīng)我大量發(fā)現(xiàn),TDD 是非常適合于編程語(yǔ)言的開發(fā)與設(shè)計(jì)。需求是未知的,易于發(fā)生變化的,還需要覆蓋足夠全的場(chǎng)景。
從實(shí)踐的層面上來說,主要是有兩種:
- 面向語(yǔ)法的測(cè)試。即,只讓語(yǔ)法編譯能通過,但是不報(bào)錯(cuò)。
- 面向功能的測(cè)試。即,驗(yàn)證某一部分的語(yǔ)法是正確的。
- 面向用例的測(cè)試。即,驗(yàn)證符合使用場(chǎng)景。
自動(dòng)化語(yǔ)言遷移
原先這部分的標(biāo)題是,向下兼容。但是,我一直覺得向下兼容不是一個(gè)好主意。所以呢,我就想了想把在其它領(lǐng)域的經(jīng)驗(yàn)搬了過來,于是呢,內(nèi)容就變成了自動(dòng)化語(yǔ)言遷移。
在關(guān)于版本的遷移上,我覺得 Angular 語(yǔ)言上關(guān)于版本的自動(dòng)化遷移,是值得我們?nèi)ソ梃b的。當(dāng)然了,采用這種設(shè)計(jì)的成本非常高,我們需要有一個(gè)專門的團(tuán)隊(duì),使用工具自動(dòng)化分析舊的系統(tǒng),并使用工具來自動(dòng)修改舊的代碼。
其它
文中相關(guān) DSL 鏈接(歡迎加入 Inherd 一起編寫 DSL):
Unflow: https://github.com/inherd/unflow
Guarding: https://github.com/inherd/guarding
Forming: https://github.com/inherd/forming
本文轉(zhuǎn)載自微信公眾號(hào)「phodal」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系phodal公眾號(hào)。