最近的一些日子裡,又陷入了平凡、無聊、繁瑣的業務程式碼開發中,生活變得無比的枯燥。每天面對著大量重複、而又沒有辦法得勝的程式碼,總會陷入憂慮之中。
而在實現幾個重複的業務程式碼時,我發現了一個更好的方式,使用領域特定語言。
最初,我是在設計一個工作流的時候,發現自己正在使用 DSL 來解決問題。因為這是一系列重複而又繁瑣的工作,所以便想著抽象出一個服務來專門做這樣的事情。
- 第一個版本里,我使用了
->
操作符來實現一個簡單的 DSL:operate -> approve -> done
。在使用的時候,我只需要傳相應的資料即可。 - 第二個版本里,我意識到並不需要這麼複雜,JavaScript object 擁有更強的語言表達能力。我只需要傳遞對應的物件過去即可,再通過
Object.keys
就可以獲取處理的順序。
於是,我就這麼將一個高大上的 DSL,變成了一個資料結構了。我一想好像不太對,JavaScript 的 object
不僅僅只是資料結構,它可以將方法作為物件中的值。隨後,我又找到了之前寫的一個表單驗證的類,也使用了類似的實現。這種動態語言特有的資料結構,也可以視之為一種特定的 DSL。
便想著寫一篇文章來介紹一下業務程式碼中的 DSL。
DSL 簡介
不過,在開始之前,相信有很多人都不知道 DSL 是什麼東西?
DSL,即領域特定語言,它是一種為解決特定領域問題,而對某個特定領域操作和概念進行抽象的語言。
在深入瞭解之前,先讓我們瞭解 DSL 的兩個大的分類:
- 外部 DSL,即建立一個專用目的的程式語言。諸如用於 BDD 測試的
Cucumber
也是一種外部 DSL,從某種意義上來說,我用於寫作的markdown
也算是一種 DSL。它們通常都需要語法解析器來進行語法分析,並且通常可以在不同的語言、平臺上實現。 - 內部 DSL,即:指與專案中使用的通用目的程式語言(Java、C#或Ruby)緊密相關的一類 DSL。它是基於通用程式語言實現的,由它來處理複雜的基礎設施和操作。^DSL
依這種定義而言,使用 JavaScript object 來實現這一類的方式,應該歸類於內部 DSL。在我寫這篇文章的時候,我總算找到了一個相關 “資料結構 DSL” 相關的介紹:
資料結構 DSL 是一種使用程式語言的資料結構構建的 DSL。其核心思想是,使用可用的基本資料結構,例如字串、數字、陣列、物件和函式,並將它們結合起來以建立抽象來處理特定的領域。
而,就實現難度而言:
資料結構 DSL < 內部 DSL < 外部 DSL < 語言工作臺
複製程式碼
這裡的資料結構 DSL,更像是一種內建函式的配置檔案。程式碼,讀的時候遠多寫的時候多。一行配置與十行程式碼相比,自然是一行配置更容易閱讀。所以,使用 object 是一種更容易的選擇。
接著,讓我愉快地展開這些 DSL 的使用歷程吧。
難以構建的外部 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
內部 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。
從外部 DSL 到內部 DSL 工作流
我喜歡 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 到資料結構 DSL
與 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…