jQuery刚刚出来的时候,我没有太多关注它,觉得这不过是Yet Another JavaScript Library。早期的jQuery专注于DOM节点的筛选与操作,不提供众多的基础类扩展,更不提供UI组件,因此体积能够做到很小。然而,我实在看不出它和我熟悉的Prototype比有什么明显的优势——jQuery能做的各项独立的操作,Prototype都能做。
后来用jQuery的人越来越多,并且大家都爱用它的链式方法调用,甚至还把这种写法推广到其它语言中去。例如ASP.NET MVP 就把他的服务器端C#组件设计为支持链式方法调用的。这时候我才开始关注jQuery,并且逐渐喜欢上了链式方法调用的写法,也在我自己的JavaScript组件中实现类似的API(参考和)。最后,我突然明白到,这其实就是一种Internal DSL嘛!
在这篇文章里,我准备先讨论Internal DSL,在下一篇文章里面再解释为什么jQuery是Internal DSL。现在我们就从最根本的问题开始吧——
什么是Internal DSL?
DSL是指Domain Specific Language,也就是用于描述和解决特定领域问题的语言。例如说,我们有专门描述字符串特征的正则表达式,有专门描述数据库查询的SQL,有专门描述XML结构的DTD和XSD,甚至有专门描述XML变换的XSLT,这些都是DSL。
当然,并非我们关注的领域都有现成的DSL,这时候我们有三个选择:
使用通用语言描述该领域的问题(non-DSL)
发明一门全新的语言描述该领域的问题()
在一门现成语言内实现针对领域问题的描述()
例如说,我们现在要描述一个很简单的金融领域问题,“我在花旗银行存款$200”这样一句话对应的三种法写法可能是:(假设已经存在I和CitiBank两个实体实例)
I.DepositTo(new USD(200), CitiBank); /* C# */
I deposit 200USD to CitiBank /* E-DSL */
I.deposit(200.USD()).to(CitiBank); /* I-DSL */
第1种做法的成本最低,你只需要有OO的思想就可以了,你总能把实体类设计出来,但可能和人类描述此领域问题的思维方式有一定偏差(为什么USD可以new?为什么不是deposit [something] to [somewhere]?)。
第2种做法的成本最高,你需要写一个全新的解释器,至少是写一组全新的规则,然后让YACC这类工具帮你生成一个解释器,但这样出来的语法最贴近人类思维方式,甚至就如同自然语言一样流畅。
第3种做法术语上述两者的折中方案,如果语法不太复杂可以使用Builder模式实现语法分析,写出来的语法相当贴近自然语言,但还是有学习门槛。由于脚本语言有相当的灵活性,所以现在很多人倾向于选择在脚本语言内实现Internal DSL。
如何构造Internal DSL?
常见的两种Internal DSL实现方法是和。如果我们需要描述一台机器的硬件组成,两种实现方式的代码分别如下:
/* Method Chaining */
computer()
.processor()
.cores(2)
.i386()
.disk()
.size(150)
.disk()
.size(75)
.speed(7200)
.sata()
.end();
/* Function Sequence */
computer();
processor();
cores(2);
processorType(i386);
disk();
diskSize(150);
disk();
diskSize(75);
diskSpeed(7200);
diskInterface(SATA);
无论是哪一种写法,中间都必须写一个分析器层。就如同语法分析器需要使用状态机一样,Internal DSL的实现也必须内置一个状态机,以记录当前执行到什么状态了,并且接下来可以转移到哪些有效状态。
由于这不是一篇专门讲语法分析器和状态机实现的文章,所以我们把关注点保持在API层面就可以了,不深入讨论其实现细节和成本。我们知道链式方法调用能够实现Internal DSL就够了,至于jQuery是如何利用好这一点的,我们在下一篇文章里再作讨论。
小结
在这篇文章里,我们了解了Internal DSL与External DSL之间的区别,同时还了解到实现Internal DSL的具体方式,这为我们接下来讨论jQuery的Internal DSL式接口做好了铺垫。在下一篇文章里,我们将深入地来看看为什么jQuery的接口要如此设计,它能为用户带来了怎样的便利,同时它自身的实现上又有什么优势。
jQuery的Internal DSL形式
在上一篇文章里面,我们了解到了Internal DSL的具体形式,形如:
/* Method Chaining */
computer()
.processor()
.cores(2)
.i386()
.disk()
.size(150)
.disk()
.size(75)
.speed(7200)
.sata()
.end();
然后我们在看看一段典型的jQuery代码:
$("ul#contacts li.item")
.find("span.name")
.click(function(e) { $(e.target).siblings(".more").toggle(); })
.end()
.find("input.delete")
.click(function(e) { $(e.target).parents(".item").remove(); })
.end()
.find("div.more")
.hide()
.end();
从结构上来说,是不是跟上面那一段Internal DSL的例子很相似?就算我们不看对应的HTML,我们也能猜到这段jQuery代码的含义:
遍历
中的每一个
(这看起来是个联系人列表)
隐藏这个div
(默认隐藏详细信息?)
绑定click事件,操作是把class="item"父节点删除
(这应该是用来删除联系人的)
绑定click事件,操作是显示/隐藏class="more"兄弟节点
(这是估计联系人姓名,点击后切换详细信息的显示/隐藏)
对于里面的
对于里面的
对于里面的
从这里我们已经能够看出jQuery的Internal DSL形式带来的好处——编写代码时,让代码更贴近作者的思维模式;阅读代码时,让读者更容易理解代码的含义。不信?我们看看与jQuery拥有相似功能的Prototype是如何实现上述逻辑:
$$("ul#contacts li.item span.name")
.invoke("observe", "click",
function(e) { $(e.target).next(".more").toggle(); });
$$("ul#contacts li.item input.delete")
.invoke("observe", "click",
function(e) { $(e.target).up(".item").remove(); });
$$("ul#contacts li.item div.more")
.invoke("hide");
这是我用Prototype所能写出的最贴近Internal DSL的形式了。(如果你能够写出一个更自然的版本,欢迎分享。)在Prototype里面,能够返回一组元素的操作就只有$$(),并且它只能作用于全局,缺乏jQuery中find()或者filter()的功能,所以这一组描述联系人列表行为的语句无法组合在一起,必须逐一定义每类元素的行为。此外,此例子中每类元素都仅仅指定了一个行为,因此Prototype的invoke()写法看起来还是和jQuery的click()写法很相近的。但如果一类元素拥有多个行为,Prototype的invoke()就不能好像jQuery那样链式调用下去了,必须每一个行为重头写一个$$(),或者把invoke()改成each()加匿名函数。无论是那种做法,都只会降低代码的可读性。
jQuery的语法分析器
我们都知道,Internal DSL的实现依赖于对语法分析器的封装,对Internal DSL的调用其实都是对语法分析器的调用,经过语法分析后再构造出对底层API的调用。例如jQuery当中的click(),它依赖于当前的状态,也就是前面$()筛选出来的节点集合,把click()解释为要为这一组节点绑定DOM的click事件,最后再调用DOM API完成任务。在这个例子当中,DOM API相对jQuery API而言就是底层API了。
jQuery可以说是挑了一个最容易实现的语法模型来做,永远只有一种token,因此永远也只有一种状态,这种状态当然也是永远有效的,你根本不可能给jQuery输入一个当前状态无效的token。jQuery的唯一状态就是一个jQuery对象实例,其本质就是一个元素集合。读入的token可能是各种针对这个元素集合的操作,但它的返回一定还是一个元素集合。这使得jQuery的语法分析器不会进入无效状态,也就无需判断无效状态,因此大大简化了Internal DSL实现中常见的一个难题。
小结
通过拿jQuery和Prototype做对比,我们可以发现jQuery用非常低的成本实现了Internal DSL,同时带来了Prototype所没有的明显好处。这可以看作是一个很好的范例——如果你需要描述的业务逻辑能够归纳为简单的语言模式,为此实现一门Internal DSL的性价比将会是很高的。你需要做的仅仅是为这个简单的语言模型实现一个简单的解释器,接着你就可以享受贴近人类思维模式的接口了。
来源:Cat Chen - 博客园