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

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

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

最初,我是在設計一個工做流的時候,發現本身正在使用 DSL 來解決問題。由於這是一系列重複而又繁瑣的工做,因此便想着抽象出一個服務來專門作這樣的事情。html

  • 第一個版本里,我使用了 -> 操做符來實現一個簡單的 DSL:operate -> approve -> done。在使用的時候,我只須要傳相應的數據便可。
  • 第二個版本里,我意識到並不須要這麼複雜,JavaScript object 擁有更強的語言表達能力。我只須要傳遞對應的對象過去便可,再經過 Object.keys 就能夠獲取處理的順序。

因而,我就這麼將一個高大上的 DSL,變成了一個數據結構了。我一想好像不太對,JavaScript 的 object 不只僅只是數據結構,它能夠將方法做爲對象中的值。隨後,我又找到了以前寫的一個表單驗證的類,也使用了相似的實現。這種動態語言特有的數據結構,也能夠視之爲一種特定的 DSL。前端

便想着寫一篇文章來介紹一下業務代碼中的 DSL。java

DSL 簡介

不過,在開始以前,相信有不少人都不知道 DSL 是什麼東西?ios

DSL,即領域特定語言,它是一種爲解決特定領域問題,而對某個特定領域操做和概念進行抽象的語言。git

在深刻了解以前,先讓咱們瞭解 DSL 的兩個大的分類:github

  • 外部 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…

相關文章
相關標籤/搜索