摘自http://coolshell.cn/articles/5709.html
程序设计语言的抽象机制包含了两个最基本的方面:一是语言关注的基本元素/语义;另一个是从基本元素/语义到复合元素/语义的构造规则。在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 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).
下面将分4个部分来逐步说明流畅接口在构造内部DSL中的典型应用。
1. 基本语义抽象
如果要输出0..4这5个数,我们一般会首先想到类似这样的代码:
1
2
3
4
|
//Java for ( int i = 0 ; i < 5 ; ++i) {
system.out.println(i);
} |
而Ruby虽然也支持类似的for循环,但最简单的是下面这样的实现:
1
2
|
//Ruby 5 .times {|i| puts i}
|
Ruby中一切皆对象,5是Fixnum类的实例,times是Fixnum的一个方法,它接受一个block参数。相比for循环实现,Ruby的times方式更简洁,可读性更强,但熟悉OOP的朋友可能会有疑问,times是否应该作为整型类的方法呢?在OOP中,方法调用通常代表了向对象发送消息,改变或查询对象的状态,times方法显然不是对整型对象状态的查询和修改。如果你是Ruby的设计者,你会把times方法放入Fixnum类吗?如果答案是否定的,那么Ruby的这种设计本质上代表了什么呢?实际上,这里的times虽然只是一个普通的类方法,但它的目的却与普通意义上的类方法不同,它的语义实际上类似于for循环这样的语言基本语义,可以被视为一种自定义的基本语义。times的语义从一定程度上跳出了类方法的框框,向问题域迈进了一步!
另一个例子来自Eric Evans的“用两个时间点构造一个时间段对象”,普通设计:
1
2
3
|
//Java TimePoint fiveOClock, sixOClock; TimeInterval meetingTime = new TimeInterval(fiveOClock, sixOClock);
|
另一种Evans的设计是这样:
1
2
|
//Java TimeInterval meetingTime = fiveOClock.until(sixOClock); |
按传统OO设计,until方法本不应出现在TimePoint类中,这里TimePoint类的until方法同样代表了一种自定义的基本语义,使得表达时间域的问题更加自然。
虽然上面的两个简单例子和普通设计相比看不出太大的优势,但它却为我们理解流畅接口打下了基础。重要的是应该体会到它们从一定程度上跳出了语言基本抽象机制的束缚,我们不应该再用类职责划分、迪米特法则(Law of Demeter)等OO设计原则来看待它们。
2. 管道抽象
在Shell中,我们可以通过管道将一系列的小命令组合在一起实现复杂的功能。管道中流动的是单一类型的文本流,计算过程就是从输入流到输出流的变换过程,每个命令是对文本流的一次变换作用,通过管道将作用叠加起来。在Shell中,很多时候我们只需要一句话就能完成log统计这样的中小规模问题。和其他抽象机制相比,管道的优美在于无嵌套。比如下面这段C程序,由于嵌套层次较深,不容易一下子理解清楚:
1
2
|
//C min(max(min(max(a,b),c),d),e) |
而用管道来表达同样的功能则清晰得多:
1
2
|
#!/bin/bash max a b | min c | max d | min e |
我们很容易理解这段程序表达的意思是:先求a, b的最大值;再把结果和c取最小值;再把结果和d求最大值;再把结果和e求最小值。
jQuery的链式调用设计也具有管道的风格,方法链上流动的是同一类型的jQuery对象,每一步方法调用是对对象的一次作用,整个方法链将各个方法的作用叠加起来。
1
2
|
//Javascript $( 'li' ).filter( ':event' ).css( 'background-color' , 'red' );
|
3. 层次结构抽象
除了管道这种“线性”结构外,流畅接口还可用于构造层次结构抽象。比如,用Javascript动态创建创建下面的HTML片段:
1
2
3
4
5
6
7
|
< 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//Javascript var 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则要有表现力得多:
1
2
3
4
5
6
7
8
9
|
//Javascript var 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();
|
4. 异步抽象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//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(); |
上面介绍了用流畅接口构造的4种典型抽象,出此之外还有很多其他的抽象和应用场合,比如:不少单元测试框架就通过流畅接口定义了单元测试的DSL。虽然上面的例子以Javascript等动态语言居多,但其实流畅接口所依赖的语法基础并不苛刻,即使在Java这样的静态语言中,同样可以轻松地使用。流畅接口不同于传统的API设计,理解和使用流畅接口关键是要突破语言抽象机制带来的定势思维,根据问题域选取适当的抽象维度,利用语言的基本语法构造领域特定的语义和语法。
参考
相关推荐
赠送jar包:dsl-1.2.7.jar;...使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准翻译,请放心使用。
使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准翻译,请放心使用。 双语对照,边学技术、边学英语。
詹金斯(Jenkins)Job DSL 所有Jenkins Job定义都集中在一个地方。 语言参考: : 访问Jenkins服务器以获取已安装的插件版本支持的当前API方法: ... Job-DSL正在使用Groovy,请访问以
内部JavaDSLBuilder
休息测试用于测试 RESTFull API 的 DSL
简单DSL Simple-DSL是一种写用于LMAX Exchange的验收测试的样式,旨在平衡人机可读性。 目的是使开发人员和非开发人员都可以轻松阅读和理解接受测试,并且开发人员IDE可以充分理解接受测试以支持有用的(但不一定是...
bupt期末DSL设计:一种领域特定脚本语言的解释器的设计与实现(java版本,代码加报告加用户说明)
这里使用的大多数构造都是在之后,但是我们包括了一些针对Neo4j的构造。 Neo4j Cypher-DSL的核心模块没有必需的运行时依赖项。版本控制自2020年初起,此Neo4j Cypher-DSL的重新启动版本以Spring所使用的相同方式...
GradleFlavor:将Android Gradle与Kotlin DSL结合使用
通用SQL到Elasticsearch DSL查询转换器。 专为设计 地位 实验性-加入我们,骇客入侵! 安装 npm install elasql 用法 const convert = require('elasql').convert convert('SELECT id,name FROM shop WHERE shop_id...
主要介绍了JavaScript DSL 流畅接口(使用链式调用)实例,本文讲解了DSL 流畅接口、DSL 表达式生成器等内容,需要的朋友可以参考下
用于 RestAPI 定义的 DSL 例子 # 예시 # @method GET # @uri /event/{number} object { # 1: 호빠 이벤트, 2: 룸싸롱 이벤트 enum display_type [1, 2]; # 기본 이벤트 정보. 이미지 URL, 링크 URL. 버튼...
这允许使用快速单元测试来测试集成点的两侧。 这个 gem 的灵感来自“消费者驱动的合同”的概念。 有关更多信息,请参阅 。 阅读以获取有关如何的更多信息。 接触 推特: Slack: 堆栈溢出: : 文档 消费者 DSL...
willa:用于Kafka流的Clojure DSL
typed-schema:Typelevel http服务定义DSL
omni:用于低级音频编程的DSL
aerospike-scala:用于Aerospike数据库的Typesafe DSL
领域语言(DSL)的设计与实践领域语言(DSL)的设计与实践领域语言(DSL)的设计与实践
但我们的设计经验工具通常仅限于在Figma / Sketch / Omnigraffle /中使用手动绘制框和箭头/无论您使用哪种思维导图或线框图工具,使用复杂JavaScript库,学习DOT或UML(不适用于设计),或仅使用自然语言。...
本体统一 本体联合是一种用于编写本体的小型 DSL(领域特定语言)。 它主要是作为一个 gem 用作项目的一部分来创建用于测试的本体,以便不依赖于固定装置。 请参阅相应的问题(ontohub/ontohub#786)。 目前它仅支持...