本文参考腾讯问卷实践——背景知识与问题介绍
想必大家都有过创建一份调查问卷的经历,调查问卷通常可以为各个问题之间设计一定的逻辑,比如下图
如果用户第一题选择了第一个选项,那么会导致第二道题目的出现。
作为一个想要发布一份问卷的用户,也就是用研,他可能需要到问卷的发布页面去点点点进行页面逻辑的配置,这不仅费时费力,而且会有一个问题,如果遇到一些比较专业且复杂的逻辑难以在UI上进行简单的交互,这时候可能就需要找问卷的开发者单独定制。然后就是下面的一连串沟通成本!
用研:Hey,麻烦帮我开发一个问卷
开发:说一下这次的需求
用研:这次是一份调查xxxx的问卷,我先会在第一题咨询xxx,如果用户xxx,那就会xxx。。。。
开发:这里没懂,这里没懂,这里没懂,这里没懂。。。。
用研:好的我解释一下,好的我解释一下,好的我解释一下,好的我解释一下
作为开发者,没有相应领域的知识,需要理解用研的需求会比较困难;作为用研,没有开发的能力导致需求修改需要频繁与开发对齐,花费大量时间。那么有没有一种方法,可以让用研不依赖开发,就可以自己定制问卷呢,答案就是DSL。
DSL全称为Domain Specific Language,译为领域特定语言,即一种为特定领域所设计的编程语言。可以简单的理解为设计DSL就是设计一套语法来描述一系列的相关行为。抽象到比较低级的层面,C++、JavaScript就是机器指令集的DSL;在比较高级的层面举例的话,markdown就是文档编写的DSL,sql语句就是数据库查询的DSL。为什么不直接用机器指令编写代码,而要使用高级语言如C++等,就是因为高级语言对于开发者的门槛更低,更容易上手,更有迹可循。究其原因,DSL是为人类设计的,而指令是为机器设计的。
下面通过一个简单的例子来说明内部DSL和外部DSL的区别。我们要怎么描述两个星期前这一个时间呢。很容易想到以下三种解法:
解法一:
new Date(Date.now() - 1000 * 60 * 60 * 24 * 7 * 2);
解法二:
2 weeks ago
解法三:
(2).weeks().ago();
作为一个前端程序员,敏锐的你肯定第一眼就看出来了第一种写法就是标准的js代码,可以直接在浏览器就能跑出结果。第二种像是一种自然语言,第三种类似一种伪代码。第二和第三种其实就是外部DSL和内部DSL了。很明显,第二种解法和第三种解法是不能直接运行的,因为DSL需要在特定的环境下运行。
很显然DSL比编程语言更加直观,而外部DSL又比内部DSL更加直观,不信你可以问问自己的男/女朋友,看他们更倾向使用哪种语法(没有男/女朋友的也可以去问问自己的产品)
为什么有外部DSL和内部DSL的区别呢?外部DSL可以理解为是一门独立的语言,比如解法二,你为了让它能够正常运行,就必须编写一套编译器来支持它运行。而对于内部DSL(解法三),你可以利用拓展现有的语言环境来支持这种语法,比如说我只要在js注入这段逻辑
Number.prototype.weeks = function() {
return this * 1000 * 60 * 60 * 24 * 7
}
Number.prototype.ago = function() {
return new Date(Date.now() - this)
}
就可以让解法三运行出结果了。受限于语言环境,内部DSL的语法也看起来比较别扭,需要遵循宿主语言的语法规则。
DSL的设计与开发也是有一定的成本的,那么什么时候需要用DSL呢?概括性的总结就是,
当用户不是系统的开发者,但又需要定制逻辑的时候,就需要一门DSL。
比如问卷的定制,再比如数据库的自定义查询。展开来说,如果一件事情有大量的重复,需要频繁的进行多方的沟通,需要很多的GUI操作,那么你就有可能需要一门DSL。
这是一份用研提出的问卷需求
这份问卷是这样的,首先会在第一题询问有没有发烧,如果选择了没有发烧,就直接跳转到结束页;然后如果选择了有, 显示第二题。第二题询问体温多少度,如果体温低于37度也 跳转到结束页...
然后我们提取出逻辑相关的内容
第一题 选择了没有发烧 跳转到结束页
选择了有 显示第二题
如果体温低于37度 跳转到结束页
转成伪代码表述
if Q1A1 then branch to END
if Q1A2 then show Q2
if Q2A < 37 then branch to END
稍微整理一下,你就可以设计出一套问卷DSL语法了
if Q1A2 then show Q2
if Q2A gt 37 then show Q3
设计你的DSL
上面讲了那么多,设计完语法之后就要动手来实现了。学过大学编译原理的我们都知道,高级语言转机器指令需要经过一系列过程:
词法分析 -> 语法分析 -> 语义分析 -> 生成抽象语法树 -> 中间代码 -> 优化 -> 目标代码
学过是学过,但要把书本上的理论知识,比如正则表达式、状态机等等再实现一次,门槛有点过于高了。
好在有一位伟人说过,不要重复造轮子。热衷于开源的大牛们开发出了一个专门用来生成DSL解析器的解析器生成器,利用它们,我们可以快速地设计实现自己的DSL而不需要熟读编译原理。比较流行的有PEG.js和jison。本文使用PEG.js进行举例,试着实现了上文中设计的问卷DSL中的其中一句 if Q1A2 then show Q2 ,原理大同小异,这里实现的比较粗糙,仅做抛砖引玉。
1. 安装引入npm包 npm i pegjs
2. 遵循pegjs规则设计语法
const grammar = `
/* 定义输入的语法格式,这里为 if xxx then xxx */
Start
= 'if' _ exp:Expression _ 'then' _ op:Operation {
/* 返回一个 输入为所有题目,输出为过滤后的题目 的函数 */
return new Function('questions', 'const result = [questions[0]];' +
'if(' + exp + ')' + op + 'return result;');
}
/* 定义Expression类型如何翻译 */
Expression "expression"
= 'Q'q:[0-9]'A'a:[0-9] {
return 'questions[' + q + '-1].answer === ' + a
}
/* 定义Operation类型如何翻译 */
Operation "operation"
= 'show Q'q:[0-9] {
return '{result.push(questions['+ q + '-1]);}'
}
/* 定义 _ 如何翻译 */
_ "whitespace"
= ' '
`;
3. parser = pegjs.generate(grammar) 生成DSL编译器
4. 然后就可以使用 parser.parse 来对输入DSL进行解析生成对应的逻辑处理函数啦
完整demo已放到codepen,大家可以在阅读原文处进行在线编辑尝试实现更多的腾讯问卷的DSL,或者创造属于自己的DSL~
PEG.js同时也支持在线生成解析器并下载下来,这样就不需要在项目中安装PEG.js库
DSL看起来很牛逼很高大上,事实上使用起来需要很大的成本,如你所见,你需要设计一套自己的语法,并为之编写生成解析器提供给用户使用;用户在使用你的DSL的时候,你极大可能需要提供一个DSL编辑器使得用户的编写体验更流畅并提供语法检查,还需要编写文档供用户查阅;对于用户来说,虽然不需要直接学习编程语言,但是不论设计的多好的DSL,依然存在学习成本,依然需要花费一定的时间才能上手你设计的DSL。
在想要使用DSL的时候,先问一问自己,能不能不使用DSL,有没有现成的标准DSL,能不能使用内部DSL。
当你确信没有更好的解法了,那就拥抱DSL吧。