流畅接口与DSL的基本概念
程序设计语言的抽象机制包含了两个最基本的方面:一是语言关注的基本元素/语义;另一个是从基本元素/语义到复合元素/语义的构造规则。在C、C++、Java、C#、Python等通用语言中,语言的基本元素/语义往往离问题域较远,通过API库的形式进行层层抽象是降低问题难度最常用的方法。
不过普通的API设计方法存在一种天然的陷阱,那就是不管怎样封装,大过程虽然比小过程抽象层次更高,但本质上还是过程,受到过程语义的制约。也就是说,通过基本元素/语义构造更高级抽象元素/语义的时候,语言的构造规则很大程度上限制了抽象的维度。
而SQL、HTML、CSS、make等DSL(领域特定语言)的抽象维度是为特定领域量身定做的,从这些抽象角度看问题往往最为简单,所以DSL在解决其特定领域的问题时比通用程序设计语言更加方便。通常,SQL等非通用语言被称为外部DSL(External DSL);在通用语言中,我们其实也可以在一定程度上突破语言构造规则的抽象维度限制,定义内部DSL(Internal DSL)。
流畅接口的核心定义
本文将介绍一种被称为流畅接口(Fluent Interface)的内部DSL设计方法。Wikipedia上Fluent Interface的定义是:
"A fluent interface (as first coined by Eric Evans and Martin Fowler) is 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 (but a fluent interface entails more than just method chaining)。"
四种典型的流畅接口抽象模式
1. 基本语义抽象
如果要输出0..4这5个数,我们一般会首先想到类似这样的代码:
java
//Javafor (int i = 0; i < 5; ++i) {
system.out.println(i);}而Ruby虽然也支持类似的for循环,但最简单的是下面这样的实现:
ruby
//Ruby5.times {|i| puts i}Ruby中一切皆对象,5是Fixnum类的实例,times是Fixnum的一个方法,它接受一个block参数。相比for循环实现,Ruby的times方式更简洁,可读性更强。
另一个例子来自Eric Evans的"用两个时间点构造一个时间段对象",普通设计:
java
//JavaTimePoint fiveOClock, sixOClock;TimeInterval meetingTime = new TimeInterval(fiveOClock, sixOClock);
另一种Evans的设计是这样:
java
//JavaTimeInterval meetingTime = fiveOClock.until(sixOClock);
按传统OO设计,until方法本不应出现在TimePoint类中,这里TimePoint类的until方法同样代表了一种自定义的基本语义,使得表达时间域的问题更加自然。
2. 管道抽象
在Shell中,我们可以通过管道将一系列的小命令组合在一起实现复杂的功能。管道中流动的是单一类型的文本流,计算过程就是从输入流到输出流的变换过程,每个命令是对文本流的一次变换作用,通过管道将作用叠加起来。
比如下面这段C程序,由于嵌套层次较深,不容易一下子理解清楚:
c
//Cmin(max(min(max(a,b),c),d),e)
而用管道来表达同样的功能则清晰得多:
bash
#!/bin/bashmax a b | min c | max d | min e
我们很容易理解这段程序表达的意思是:先求a,b的最大值;再把结果和c取最小值;再把结果和d求最大值;再把结果和e求最小值。
jQuery的链式调用设计也具有管道的风格,方法链上流动的是同一类型的jQuery对象,每一步方法调用是对对象的一次作用,整个方法链将各个方法的作用叠加起来。
javascript
//Javascript$('li').filter(':event').css('background-color', 'red');3. 层次结构抽象
除了管道这种"线性"结构外,流畅接口还可用于构造层次结构抽象。比如,用Javascript动态创建创建下面的HTML片段:
html
<div id="'product_123'" class="'product'"> <img src="'preview_123.jpg'" alt="" /> <ul> <li>Name: iPad2 32G</li> <li>Price: 3600</li> </ul></div>
若采用Javascript的DOM API:
javascript
//Javascriptvar div = document.createElement('div');div.setAttribute('id', 'product_123');div.setAttribute('class', 'product');var img = document.createElement('img');img.setAttribute('src', 'preview_123.jpg');div.appendChild(img);var ul = document.createElement('ul');var li1 = document.createElement('li');var txt1 = document.createTextNode("Name: iPad2 32G");li1.appendChild(txt1);//...div.appendChild(ul);而下面流畅接口API则要有表现力得多:
javascript
//Javascriptvar obj =$.div({id:'product_123', class:'product'})
.img({src:'preview_123.jpg'})
.ul()
.li().text('Name: iPad2 32G')._li()
.li().text('Price: 3600')._li()
._ul()._div();和Javascript的标准DOM API相比,上面的API设计不再局限于孤立地看待某一个方法,而是考虑了它们在解决问题时的组合使用,所以代码的表现形式特别贴近问题的本质。这样的代码是自解释的(self-explanatory)在可读性方面要明显胜于DOM API,这相当于定义了一种类似于HTML的内部DSL,它拥有自己的语义和语法。
4. 异步抽象
流畅接口不仅可以构造复杂的层次抽象,还可以用于构造异步抽象。在基于回调机制的异步模式中,多个异步调用的同步和嵌套问题是使用异步的难点所在。有时一个稍复杂的调用和同步关系会导致代码充满了复杂的同步检查和层层回调,难以理解和维护。
针对这个问题,可以用Javascript编写一个基于流畅接口的异步DSL,示例代码如下:
javascript
//Javascript$.begin()
.async(newTask('task1'), 'task1')
.async(newTask('task2'), 'task2')
.async(newTask('task3'), 'task3').when()
.each_done(function(name, result) {
console.log(name + ': ' + result);
})
.all_done(function(){
console.log('good, all completed');
})
.timeout(function(){
console.log('timeout!!');
$.begin()
.async(newTask('task4'), 'task4')
.when()
.each_done(function(name, result) {
console.log(name + ': ' + result);
})
.end();
}, 3000).end();上面的代码只是一句Javascript调用,但从另一个角度看它却像一段描述异步调用的DSL程序。它通过流畅接口定义了begin when end的语法结构,begin后面跟的是启动异步调用的代码;when后面是异步结果处理,可以选择each_done, all_done, timeout中的一种或多种。而begin when end结构本身是可以嵌套的。
流畅接口的设计原则
突破传统OO设计限制
理解和使用流畅接口关键是要突破语言抽象机制带来的定势思维,根据问题域选取适当的抽象维度,利用语言的基本语法构造领域特定的语义和语法。不应该再用类职责划分、迪米特法则(Law of Demeter)等OO设计原则来看待它们。
上下文保持与类型流动
在流畅接口设计中,方法链的每一步都应该保持恰当的上下文,并确保类型的正确流动。在管道抽象中,方法链通常返回同一类型的对象;而在层次结构抽象中,返回类型可能随着层次深度而变化。
领域语义表达
流畅接口的成功关键在于能否准确表达领域语义。API设计者需要深入理解问题域,找到最适合的抽象维度,使代码能够直观反映领域概念和业务规则。
实际应用场景
测试框架DSL
不少单元测试框架就通过流畅接口定义了单元测试的DSL,例如:
java
//Java@Testpublic void testUserRegistration() {
given()
.contentType(JSON)
.body(userData)
.when()
.post("/users")
.then()
.statusCode(201)
.body("id", notNullValue())
.body("username", equalTo("testuser"));}数据查询DSL
在数据访问层,流畅接口可以用于构造类型安全的查询DSL:
java
//JavaList<User> users = query .select(USER.NAME, USER.EMAIL)
.from(USER)
.where(USER.AGE.greaterThan(18))
.and(USER.CITY.equalTo("Beijing"))
.orderBy(USER.NAME.asc())
.fetch();结语
流畅接口作为一种内部DSL设计方法,通过方法链和上下文保持机制,使API能够更好地表达领域语义,提高代码的可读性和表现力。虽然上面的例子以Javascript等动态语言居多,但其实流畅接口所依赖的语法基础并不苛刻,即使在Java这样的静态语言中,同样可以轻松地使用。
通过基本语义抽象、管道抽象、层次结构抽象和异步抽象等模式,流畅接口为我们提供了一种突破传统API设计限制的新思维方式,有助于创建更加优雅和易于使用的API设计。
重点提示:
流畅接口通过方法链实现更可读的代码
突破传统OO设计原则的限制,专注于领域表达
四种典型抽象模式:基本语义、管道、层次结构和异步
内部DSL拥有自己的语义和语法,贴近问题本质
适用于测试框架、数据查询、UI构建等多种场景
静态语言和动态语言都能有效实现流畅接口


网站品牌策划:深度行业分析+用户画像定位,制定差异化品牌策略

