如何從業務代碼中提升技術:使用領域特定語言消除重複代碼

最近的一些日子裡,又陷入了平凡、無聊、繁瑣的業務代碼開發中,生活變得無比的枯燥。每天面對著大量重複、而又沒有辦法得勝的代碼,總會陷入憂慮之中。

而在實現幾個重複的業務代碼時,我發現了一個更好的方式,使用領域特定語言。

最初,我是在設計一個工作流的時候,發現自己正在使用 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。它是基於通用編程語言實現的,由它來處理複雜的基礎設施和操作。1

依這種定義而言,使用 JavaScript object 來實現這一類的方式,應該歸類於內部 DSL。在我寫這篇文章的時候,我總算找到了一個相關 “數據結構 DSL” 相關的介紹:

數據結構 DSL 是一種使用編程語言的數據結構構建的 DSL。其核心思想是,使用可用的基本數據結構,例如字符串、數字、數組、對象和函數,並將它們結合起來以創建抽象來處理特定的領域。

而,就實現難度而言:

數據結構 DSL < 內部 DSL < 外部 DSL < 語言工作臺

這裡的數據結構 DSL,更像是一種內置函數的配置文件。代碼,讀的時候遠多寫的時候多。一行配置與十行代碼相比,自然是一行配置更容易閱讀。所以,使用 object 是一種更容易的選擇。

接著,讓我愉快地展開這些 DSL 的使用歷程吧。

難以構建的外部 DSL

某些外部 DSL,看上去已經可以說是一門編程語言了,它也可以編譯為可執行的程序,也可以是邊運行邊解釋,類似於解釋型語言。不過,它通常是作為程序的一部分存在的,如 Emacs Lisp,可以編譯為程序,但是多數時候是作為 Emacs 的一部分而存在。

這算得上是一種複雜的 DSL,而簡單的外部 DSL,而諸如我們平時開發用的前端模板:

  {topic.attributes.commentsCount}

對於這樣一個模板來說,我們要做的就是使用 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:上述的簡化代碼見:https://github.com/phodal/oadsl

在業務複雜的情況下,則可以有針對性的設計出這樣的 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 內部了。

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:https://github.com/phodal/oadsl

如何從業務代碼中提升技術:使用領域特定語言消除重複代碼


分享到:


相關文章: