最近的一些日子裏,又陷入了平凡、無聊、繁瑣的業務代碼開發中,生活變得無比的枯燥。天天面對着大量重複、而又沒有辦法得勝的代碼,總會陷入憂慮之中。javascript
而在實現幾個重複的業務代碼時,我發現了一個更好的方式,使用領域特定語言。css
最初,我是在設計一個工做流的時候,發現本身正在使用 DSL 來解決問題。由於這是一系列重複而又繁瑣的工做,因此便想着抽象出一個服務來專門作這樣的事情。html
->
操做符來實現一個簡單的 DSL:operate -> approve -> done
。在使用的時候,我只須要傳相應的數據便可。Object.keys
就能夠獲取處理的順序。因而,我就這麼將一個高大上的 DSL,變成了一個數據結構了。我一想好像不太對,JavaScript 的 object
不只僅只是數據結構,它能夠將方法做爲對象中的值。隨後,我又找到了以前寫的一個表單驗證的類,也使用了相似的實現。這種動態語言特有的數據結構,也能夠視之爲一種特定的 DSL。前端
便想着寫一篇文章來介紹一下業務代碼中的 DSL。java
不過,在開始以前,相信有不少人都不知道 DSL 是什麼東西?ios
DSL,即領域特定語言,它是一種爲解決特定領域問題,而對某個特定領域操做和概念進行抽象的語言。git
在深刻了解以前,先讓咱們瞭解 DSL 的兩個大的分類:github
Cucumber
也是一種外部 DSL,從某種意義上來講,我用於寫做的 markdown
也算是一種 DSL。它們一般都須要語法解析器來進行語法分析,而且一般能夠在不一樣的語言、平臺上實現。依這種定義而言,使用 JavaScript object 來實現這一類的方式,應該歸類於內部 DSL。在我寫這篇文章的時候,我總算找到了一個相關 「數據結構 DSL」 相關的介紹:編程
數據結構 DSL 是一種使用編程語言的數據結構構建的 DSL。其核心思想是,使用可用的基本數據結構,例如字符串、數字、數組、對象和函數,並將它們結合起來以建立抽象來處理特定的領域。數組
而,就實現難度而言:
數據結構 DSL < 內部 DSL < 外部 DSL < 語言工做臺
複製代碼
這裏的數據結構 DSL,更像是一種內置函數的配置文件。代碼,讀的時候遠多寫的時候多。一行配置與十行代碼相比,天然是一行配置更容易閱讀。因此,使用 object 是一種更容易的選擇。
接着,讓我愉快地展開這些 DSL 的使用歷程吧。
某些外部 DSL,看上去已經能夠說是一門編程語言了,它也能夠編譯爲可執行的程序,也能夠是邊運行邊解釋,相似於解釋型語言。不過,它一般是做爲程序的一部分存在的,如 Emacs Lisp,能夠編譯爲程序,可是多數時候是做爲 Emacs 的一部分而存在。
這算得上是一種複雜的 DSL,而簡單的外部 DSL,而諸如咱們平時開發用的前端模板:
<View style={{ flexDirection: 'row' }}>
<Icon style={{ marginLeft: 5, marginRight: 5 }} name={'ios-chatboxes-outline'} type={'ionicon'} color={'#333'} />
<Text>{topic.attributes.commentsCount}</Text>
</View>
複製代碼
對於這樣一個模板來講,咱們要作的就是使用 JavaScript 實現一個解析器。在構建的時候,將其編譯爲 JavaScript 代碼;在運行的時候,再將其轉換爲 HTML。
以我幾回、有限的建立 DSL 的經從來說,諸如:stepping,我以爲外部 DSL 並不容易實現——雖然已經有了 Flex 和 Bison(在 JavaScript 世界裏,有一個名爲 Jison 的實現)這樣的工具。其至關因而本身寫一個編程語言,與此同時設計出一個容易使用的語法。
如我以前設計用於 DDD 的 stepping
看上去就像是一個配置文件,而我是使用 Jison 寫了本身的語法分析:
domain: 庫存子域
aggregate: 庫存
event: 庫存已增長
event: 庫存已恢復
event: 庫存已扣減
event: 庫存已鎖定
command: 編輯庫存
複製代碼
Whatever,要實現這樣一個 DSL 並非一件容易的事。就目前而言,使用最普遍的 DSL,恐怕要數 markdown
了?
固然了,對於大的項目,或者大的組織團隊來講,要實現這樣一個 DSL 並非問題。它也有利於組織內部的溝通,DSL 在這裏就像是一個領域知識的存在。
而就使用習慣來講,更常見的是內部 DSL。
內部 DSL,一般由編程語言內部來實現,一種常見的實現方式就是:流暢(fluent)接口。如,jQuery 就是這種內部 DSL 的典型的例子。
$('.mydiv')
.addClass('flash')
.draggable()
.css('color', 'blue')
複製代碼
內部 DSL 是在一門現成語言內,實現針對領域問題的描述。如上述代碼中的 jQuery 語法就是專用於 DOM 處理的,它的 API 也就是其最出名的鏈式方法調用
。
以下,也是一種內部 DSL 的實現:
var query =
SQL('select name, desc from widgets')
.WHERE('price < ', $(params.max_price), AND,
'clearance = ', $(params.clearance))
.ORDERBY('name asc');
複製代碼
而對於咱們實現來講,則多是:
function SQL (param) {
this.WHERE = function(){
return this;
};
this.ORDERBY = function(){
return this;
};
return this;
}
複製代碼
這種 DSL 專門針對的是開發人員的使用,對於複雜、重複應用來講,它特別有幫助。能夠設計出專用於業務的 DSL。
可問題來了,在前端領域的業務代碼裏,要實現這樣一個 DSL 的機會並不大——一個合理的項目來講,複雜的業務邏輯應該由 BFF 層實現,內部 DSL 更常見於框架的 API 設計上。除非,咱們在設計一個框架,諸如 Jasmine,這樣的測試框架:
const simDescribe = (desc: any, fn: any) => {
console.log(desc)
fn()
}
const simIt = (msg: any, fn: any) => {
simDescribe(' ' + msg, fn)
}
...
export const SimTest = {
describe: simDescribe,
expect: simExpect,
...
}
複製代碼
PS:上述的簡化代碼見:github.com/phodal/oads…
在業務複雜的狀況下,則能夠有針對性的設計出這樣的 API。
我喜歡 JavaScript、Python 這一類動態語言,是由於其擁有優秀的語言表達力。而 JavaScript 這門語言在一點上,那便更爲突出。JSON 和 JavaScript Object 能夠幫助咱們快速地建立這樣的一個 DSL。
最初,我產生了一個 DSL 的想法是由於:Angular 框架的動畫形式的:void => inactive
,或者是 inactive => active
的形式。這讓我聯想到了一個工做流能夠這麼設計:
process = 'transact -> approve -> bank';
複製代碼
對應的,咱們只須要寫相應的數據便可:
[{
name: 'transact',
icon: 'success'
},{
name: 'approve',
icon: 'processing'
},{
name: 'bank',
icon: 'todo'
}]
複製代碼
(PS:如今看來除了幫助我寫文章,它的意義並無那麼重要。)
可是這樣的 DSL,並不容易使用。爲了使用它,咱們須要一個數據,一個流程,兩個參數。而咱們面向的是開發人員,越簡單地 API 也就越容易使用。而 JavaScript 裏的 object 正好能夠起一個順序的做用,咱們保須要使用 Object.keys
就能夠獲取到對應的值。其對應的實現也比較簡單(簡化版本):
export function workflowParser(data: any) {
const keys = Object.keys(data)
const results = []
for (let key of keys) {
let process = data[key] as IWorkflow
results.push({
name: process.name,
status: process.status,
icon: `icon-${process.status}`
})
}
return results
}
複製代碼
對應的咱們只須要一個參數:
transact: {...},
approve: {...},
bank: {...}
複製代碼
因而,一個有點複雜的 DSL 就變成了一個 Object。而更像是一個 JSON,隨後咱們只須要定義好一系列的流程,而後獲取便可:
<process data={{WorkflowMap.SUCCESS}}></process>
複製代碼
這樣一來,咱們就將複雜度轉移到了組件 process 內部了。
與 JSON 相比,JavaScript Object 有一點至關的迷人,便可以支持使用函數。
除了組件上的重用,還有一種常見的例子就是:表單驗證。表單驗證是一種至關繁瑣的工做,咱們也能夠看到一系列相應的 DSL 實現。以下是一個用於表單驗證的 DSL:
const LoginFormValidateMap = {
phone: {
require: true,
regular: RegexMap.phone
},
country: {
requireBy: 'phone'
},
email: {
requireByNot: {
country: 'CN'
}
}
}
複製代碼
它與 JSON 形式不一樣的是,咱們能夠動態修改對象中的值,傳入函數。其實現與 JSON 的示例來講,也同樣的簡單。咱們就只須要遍歷這些值便可:
export function FormValidator(validateMap: any, data: any) {
let validateKeys = Object.keys(validateMap)
for (const key of validateKeys) {
const map = validateMap[key] as IValidate
if (map.require) {
if (!data[key]) {
return {
key: key,
error: VALIDATE_ERROR.REQUIRE
}
}
}
...
}
}
複製代碼
而後,就能夠驗證字段是否有錯:
const data = {
phone: '1234567980',
country: 'US',
email: ''
}
let result = FormValidator(LoginFormValidateMap, data)
複製代碼
上述的實現是爲了解析方便。一個更加 DSL 的實現,應該是:
const methods = [
['不能爲空', isNotEmpty],
['不得長於', isNotLongerThan]
]
複製代碼
而後,咱們只須要對應於咱們的錯誤信息,寫一個 ${key} 不能爲空
便可。
如咱們所看到的,要實現這樣一個 DSL 並不困難。由於難的並非去作這樣的設計,而是這種保持設計的思惟。隨後,不斷的練習掌握好如何去設計一個 DSL。
當下次咱們遇到這樣的場景時,是否會想:有沒有更好的實現方法?
若是有更充裕的時間,我想設計一些更優雅、容易使用的 DSL:github.com/phodal/oads…