程序设计语言的抽象机制包括了两个最基本的方面:一是语言关注的基本元素/语义;另一个是从基本元素/语义到复合元素/语义的架构规则。在C、C++、Java、C#、Python等通用语言中,语言的基本元素/语义总是离问题域较远,通过API库的形式进行层层抽象是减少问题困难程度最常见的办法。譬如,在C语言中最容易见到的方法是提供函数库来封装复杂逻辑,便捷外部调用。
不过普通的API设计办法存在一种天然的陷阱,那就是不管如何封装,大过程虽然比小过程抽象层次更高,但本质上还是过程,遭到过程语义的制约。也就是说,通过基本元素/语义架构更高级抽象元素/语义的时候,语言的架构规则非常大程度上限制了抽象的维度,大家非常难跳出这个维度去,甚至可能根本意识不到这个限制。而SQL、HTML、CSS、make等DSL(范围特定语言)的抽象维度是为特定范围量身订做的,从这类抽象角度看问题总是最为简单,所以DSL在解决其特定范围的问题时比通用程序设计语言愈加便捷。一般,SQL等非通用语言被叫做外部DSL(External DSL);在通用语言中,大家其实也可以在一定量上突破语言架构规则的抽象维度限制,概念内部DSL(Internal DSL)。
本文将介绍一种被叫做流畅接口(Fluent Interface)的内部DSL设计办法。Wikipedia上Fluent Interface的概念是:
A fluent interfaceis an implementation of an object oriented API that aims to provide for more readable code. A fluent interface is normally implemented by using method chaining to relay the instruction context of a subsequent call 。
下面将分4个部分来逐步说明流畅接口在架构内部DSL中的典型应用。
1.基本语义抽象
假如要输出0..4这5个数,大家一般会第一想到类似如此的代码:
//Java for{ system.out.println; }而Ruby虽然也支持类似的for循环,但最简单的是下面如此的达成:
//Ruby .times{|i|putsi}Ruby中所有皆对象,5是Fixnum类的实例,times是Fixnum的一个办法,它同意一个block参数。相比for循环达成,Ruby 的times方法更简洁,可读性更强,但熟知OOP的朋友或许会有疑问,times是不是应该作为整型类的办法呢?在OOP中,办法调用一般代表了向对象发送消息,改变或查看对象的状况,times办法显然不是对整型对象状况的查看和修改。假如你是Ruby的设计者,你会把times办法放入Fixnum类吗?假如答案是相反的,那样Ruby的这种设计本质上代表了什么呢?事实上,这里的times虽然只不过一个普通的类办法,但它的目的却与普通意义上的类办法不同,它的语义事实上像for循环如此的语言基本语义,可以被视为一种自概念的基本语义。times的语义从一定量上跳出了类办法的框框,向问题域迈进了一步!
另一个例子来自Eric Evans的用两个时间点架构一个时间段对象,普通设计:
3//Java TimePointfiveOClock,sixOClock; TimeIntervalmeetingTime=newTimeInterval;另一种Evans的设计是如此:
2//Java TimeIntervalmeetingTime=fiveOClock.until;按传统OO设计,until办法本不应出目前TimePoint类中,这里TimePoint类的until办法同样代表了一种自概念的基本语义,使得表达时间域的问题愈加自然。
虽然上面的两个简单例子和普通设计相比看不出太大的优势,但它却为大家理解流畅接口打下了基础。要紧的是应该领会到它们从一定量上跳出了语言基本抽象机制的束缚,大家不应该再用类职责划分、迪米特法则(Law of Demeter)等OO设计原则来看待它们。
2.管道抽象
在Shell中,大家可以通过管道将一系列的小命令组合在一块达成复杂的功能。管道中流动的是单一种类的文本流,计算过程就是从输入流到输出流的变换过程,每一个命令是对文本流的一次变换用途,通过管道将用途叠加起来。在Shell中,有时候大家仅需一句话就能完成log统计如此的中小规模问题。和其他抽象机制相比,管道的优美在于无嵌套。譬如下面这段C程序,因为嵌套层次较深,困难一下子理解了解:
2//C min,c),d),e)而用管道来表达同样的功能则明确得多:
2#!/bin/bash maxab|minc|maxd|mine大家比较容易理解这段程序表达的意思是:先求a,b的最大值;再把结果和c取最小值;再把结果和d求最大值;再把结果和e求最小值。
jQuery的链式调用设计也具备管道的风格,办法链上流动的是同一种类的jQuery对象,每一步办法调用是对对象的一次用途,整个办法链将每个办法有哪些用途叠加起来。
2//Javascript $.filter.css;3.层次结构抽象
除去管道这种线性结构外,流畅接口还可用于架构层次结构抽象。譬如,用Javascript动态创建创建下面的HTML片段:
<pid=product_123class=product><imgsrc=preview_123.jpgalt=/><ul><li>Name:iPad232G</li><li>Price:3600</li></ul></p>若使用Javascript的DOM API:
//Javascript varp=document.createElement; p.setAttribute; p.setAttribute; varimg=document.createElement; img.setAttribute; p.appendChild; varul=document.createElement; varli1=document.createElement; vartxt1=document.createTextNode; li1.appendChild; … p.appendChild;而下面流畅接口API则要有表现力得多:
//Javascript varobj= $.p .img .ul .li.text._li .li.text._li ._ul ._p;和Javascript的规范DOM API相比,上面的API设计不再局限于孤立地看待某一个办法,而是考虑了它们在解决问题时的组合用,所以代码的表现形式特别贴近问题的本质。如此的代码是自讲解的(self-explanatory)在可读性方面要明显胜于DOM API,这等于概念了一类型似于HTML的内部DSL,它拥有我们的语义和语法。需要特别注意的是,上面的层次结构抽象和管道抽象有着本质的不同,管道抽象的办法链上一般是同一对象的连续传递,而层次抽象中办法链上的对象却在伴随层次的变化而变化。此为,大家可以把业务规则也表达在流畅接口中,譬如上面的例子中,body不可以包括在p返回的对象中,p.body将抛出body办法没有异常。
4.异步抽象
流畅接口不仅能够架构复杂的层次抽象,还可以用于架构异步抽象。在基于回调机制的异步模式中,多个异步调用的同步和嵌套问题是用异步的难题所在。有时一个稍复杂的调用和同步关系会致使代码充满了复杂的同步检查和层层回调,很难理解和维护。这个问题从本质上讲和上面HTML的例子一样,是因为多数通用语言并未把异步作为基本元素/语义,很多异步达成模式是向语言的妥协。针对这个问题,我用Javascript撰写了一个基于流畅接口的异步DSL,示例代码如下:
//Javascript $.begin .async,'task1') .async,'task2') .async,'task3') .when .each_done{ console.log;}) .all_done{console.log;}) .timeout{ console.log; $.begin .async,'task4') .when .each_done{ console.log;}) .end;} ,3000) .end;上面的代码只不过一句Javascript调用,但从另一个角度看它却像一段描述异步调用的DSL程序。它通过流畅接口概念了begin when end的语法结构,begin后面跟的是启动异步调用的代码;when后面是异步结果处置,可以选择each_done, all_done, timeout中的一种或多种。而begin when end结构本身是可以嵌套的,譬如上面的代码在timeout处置分支中就包括了另一个begin when end结构。通过这个DSL,大家可以比基于回调的方法更好地表达异步调用的同步和嵌套关系。
上面介绍了用流畅接口架构的4种典型抽象,出此以外还有不少其他的抽象和应用场所,譬如:不少单元测试框架就通过流畅接口概念了单元测试的DSL。虽然上面的例子以Javascript等动态语言居多,但其实流畅接口所依靠的语法基础并不苛刻,即便在Java如此的静态语言中,同样可以轻松地用。流畅接口不同于传统的API设计,理解和用流畅接口重点是要突破语言抽象机制带来的定势思维,依据问题域选取适合的抽象维度,借助语言的基本语法架构范围特定的语义和语法。