最近的一些日子里,又陷入了平凡、无聊、繁琐的业务代码开发中,生活变得无比的枯燥。天天面对着大量重复、而又没有办法得胜的代码,总会陷入忧虑之中。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…