程序设计语言的抽象机制包含了两个最基本的方面:一是语言关注的基本元素/语义;另一个是从基本元素/语义到复合元素/语义的构造规则。在C、C++、Java、C#、Python等通用语言中,语言的基本元素/语义往往离问题域较远,通过API库的形式进行层层抽象是降低问题难度最常用的方法。比如,在C语言中最常见的方式是提供函数库来封装复杂逻辑,方便外部调用。
不过普通的API设计方法存在一种天然的陷阱,那就是不管怎样封装,大过程虽然比小过程抽象层次更高,但本质上还是过程,受到过程语义的制约。也就是说,通过基本元素/语义构造更高级抽象元素/语义的时候,语言的构造规则很大程度上限制了抽象的维度,我们很难跳出这个维度去,甚至可能根本意识不到这个限制。而SQL、HTML、CSS、make等DSL(领域特定语言)的抽象维度是为特定领域量身定做的,从这些抽象角度看问题往往最为简单,所以DSL在解决其特定领域的问题时比通用程序设计语言更加方便。通常,SQL等非通用语言被称为外部DSL(ExternalDSL);在通用语言中,我们其实也可以在一定程度上突破语言构造规则的抽象维度限制,定义内部DSL(Internal
DSL)。
本文将介绍一种被称为流畅接口(Fluent Interface)的内部DSL设计方法。Wikipedia上Fluent Interface的定义是:
A fluent interface (as first coined by Eric Evans andMartin Fowler) is an implementation of an object oriented API that aimsto provide for more readable code. A fluent interface is normallyimplemented by using method chaining to relay the instruction contextof
a subsequent call (but a fluent interface entails more than justmethod chaining).
下面将分4个部分来逐步说明流畅接口在构造内部DSL中的典型应用。
1. 基本语义抽象
如果要输出0..4这5个数,我们一般会首先想到类似这样的代码:
1
2
3
4
|
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
|
TimePoint fiveOClock, sixOClock;
TimeInterval meetingTime =
new TimeInterval(fiveOClock, sixOClock);
|
另一种Evans的设计是这样:
1
2
|
TimeInterval meetingTime = fiveOClock.until(sixOClock);
|
按传统OO设计,until方法本不应出现在TimePoint类中,这里TimePoint类的until方法同样代表了一种自定义的基本语义,使得表达时间域的问题更加自然。
虽然上面的两个简单例子和普通设计相比看不出太大的优势,但它却为我们理解流畅接口打下了基础。重要的是应该体会到它们从一定程度上跳出了语言基本抽象机制的束缚,我们不应该再用类职责划分、迪米特法则(Law of Demeter)等OO设计原则来看待它们。
2. 管道抽象
在Shell中,我们可以通过管道将一系列的小命令组合在一起实现复杂的功能。管道中流动的是单一类型的文本流,计算过程就是从输入流到输出流的变换过程,每个命令是对文本流的一次变换作用,通过管道将作用叠加起来。在Shell中,很多时候我们只需要一句话就能完成log统计这样的中小规模问题。和其他抽象机制相比,管道的优美在于无嵌套。比如下面这段C程序,由于嵌套层次较深,不容易一下子理解清楚:
1
2
|
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
|
$( '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
|
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
|
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();
|
和Javascript的标准DOMAPI相比,上面的API设计不再局限于孤立地看待某一个方法,而是考虑了它们在解决问题时的组合使用,所以代码的表现形式特别贴近问题的本质。这样的代码是自解释的(self-explanatory)在可读性方面要明显胜于DOMAPI,这相当于定义了一种类似于HTML的内部DSL,它拥有自己的语义和语法。需要特别注意的是,上面的层次结构抽象和管道抽象有着本质的不同,管道抽象的方法链上通常是同一对象的连续传递,而层次抽象中方法链上的对象却在随着层次的变化而变化。此为,我们可以把业务规则也表达在流畅接口中,比如上面的例子中,body()不能包含在div()返回的对象中,div().body()将抛出”body方法不存在”异常。
4. 异步抽象
流畅接口不仅可以构造复杂的层次抽象,还可以用于构造异步抽象。在基于回调机制的异步模式中,多个异步调用的同步和嵌套问题是使用异步的难点所在。有时一个稍复杂的调用和同步关系会导致代码充满了复杂的同步检查和层层回调,难以理解和维护。这个问题从本质上讲和上面HTML的例子一样,是由于多数通用语言并未把异步作为基本元素/语义,许多异步实现模式是向语言的妥协。针对这个问题,我用Javascript编写了一个基于流畅接口的异步DSL,示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
$.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程序。它通过流畅接口定义了beginwhen end的语法结构,begin后面跟的是启动异步调用的代码;when后面是异步结果处理,可以选择each_done,all_done, timeout中的一种或多种。而begin whenend结构本身是可以嵌套的,比如上面的代码在timeout处理分支中就包含了另一个begin whenend结构。通过这个DSL,我们可以比基于回调的方式更好地表达异步调用的同步和嵌套关系。
上面介绍了用流畅接口构造的4种典型抽象,出此之外还有很多其他的抽象和应用场合,比如:不少单元测试框架就通过流畅接口定义了单元测试的DSL。虽然上面的例子以Javascript等动态语言居多,但其实流畅接口所依赖的语法基础并不苛刻,即使在Java这样的静态语言中,同样可以轻松地使用。流畅接口不同于传统的API设计,理解和使用流畅接口关键是要突破语言抽象机制带来的定势思维,根据问题域选取适当的抽象维度,利用语言的基本语法构造领域特定的语义和语法。
参考
分享到:
相关推荐
内部DSL是通过在已有的编程语言内部构造一种专用的语言来实现的,使得代码更贴近所要解决的问题领域。Martin Fowler是一位知名的技术作家和软件咨询师,他在他的著作中多次探讨了DSL的设计与实现。 首先,让我们...
标题 "mellifluent-core: 自动生成Java内部DSL,而无需添加注释" 提供了关于一个特定的Java开发工具的信息。这个工具名为mellifluent-core,它是一款Maven插件,旨在帮助开发者创建更流畅的API,并通过构建者模式...
总的来说,KHttp通过Kotlin DSL封装OKHttp3,实现了优雅的API设计,让Android开发者在进行网络请求时,能够写出更加简洁、易读的代码,提升了开发效率。结合实际项目中的需求,你可以根据KHttp提供的基础,进行...
kohttp库默认可能使用内部线程池来处理网络请求,确保请求在合适的线程上运行,避免了对应用性能的影响。 8. **与其他库集成** kohttp可以轻松地与Kotlin的其他库集成,如Coroutines用于协程支持,或者Retrofit等...
- **Anko库**:简化Android原生API的使用,提供DSL(领域特定语言)来替代XML布局。 - **Dsl for Android Manifest**:使用Kotlin编写AndroidManifest.xml,提高可读性和可维护性。 - **Coroutines**:处理异步...
在编程领域,Fluent API是一种设计模式,它允许开发者创建具有流畅、易读的接口,使得代码更加简洁和可读性更强。这种模式通常用于构建领域特定语言(DSL)或者配置对象。在C#中,Fluent API广泛应用于构建框架和库...
1. **更少的样板代码**:Kotlin的语法设计减少了大量Java中的常见样板代码,如构造器、getter/setter等。 2. **互操作性**:Kotlin可以无缝地与现有的Java代码库一起工作,这意味着迁移现有项目到Kotlin时,不必...
5. 网络请求:使用Kotlin配合Retrofit或OkHttp进行网络请求,可以构建更加简洁和可读的API接口调用。 6. Coroutines:在Android开发中,Kotlin的协程(Coroutines)提供了一种高效处理异步任务的方式,可以解决回调...
- **类与对象**:Kotlin中的类支持构造函数、继承、接口实现等特性,同时还有数据类简化数据对象的创建。 2. **空安全** - **可空与非空类型**:Kotlin通过?符号区分可空与非空类型,避免了Java中的空指针异常。 ...
- Android KTX库提供了Kotlin风格的API,使得Android开发更加流畅。 - 使用Kotlin进行Android开发可以显著减少代码量,提高代码质量。 通过深入学习和实践《Kotlin for Android Developers》中的内容,开发者将...
Kotlin中的接口允许有默认方法和抽象方法,可以直接在接口中提供实现,简化了多态性设计。 ### 15. **内联函数(Inline Functions)** 内联函数可以消除函数调用的开销,特别是对于高阶函数,可以显著提升性能。 ...
使用 FluentOMatic,程序员可以轻松地创建自己的 DSL(领域特定语言)或者增强现有的 API,以提供更自然的编程体验。在 .NET 开发中,DSLs 能够简化复杂任务,使得代码更贴近业务逻辑,提高代码的可读性和可维护性。...
2. **DSL支持**:使用Kotlin的内联函数和类型安全的构造函数,Helios提供了流畅的DSL,使得构建JSON对象变得直观且易于阅读。例如,你可以直接用Kotlin代码来表示JSON结构,如`json { "key" to "value" }`。 3. **...
- **类与对象**:Kotlin中的类定义、继承、接口以及构造函数等面向对象特性在项目中必不可少,可能有星球、角色、飞船等实体类的设计。 2. **Kotlin的类型系统**: - **类型推断**:Kotlin能够自动推断变量和参数...
9. **Room Persistence Library**:对于本地数据存储,GoFinances可能使用Room,它是SQLite的高级封装,提供类型安全的查询和流畅的数据库操作API。 10. **MVVM(Model-View-ViewModel)**:MVVM是一种设计模式,有...
- **视图动画**:在API 1以下的版本中使用,主要是改变视图的位置、大小、透明度等,不改变视图的实际属性。 - **过渡动画**:在Android 5.0引入,用于Activity之间的过渡效果,可以自定义转场动画。 4. 创建...
8. **Android KTX**:这是官方提供的Kotlin扩展库,进一步简化了Android API的使用,使代码更符合Kotlin的风格。 在实际的Android应用开发过程中,你可能会看到以下Kotlin的使用场景: - **Activity和Fragment**:...
例如,我们可以用Kotlin的data class简化数据模型,用密封类(Sealed Class)处理枚举类型的扩展,或者使用lambda表达式来简化事件监听器的实现。 “TestAV”可能指的是音频视频相关的测试,这涉及到多媒体处理技术...
在"Tulsi-Distributors"项目的源代码中,我们可以期待看到Kotlin如何应用于模块化设计、错误处理、类型安全以及与其他服务(如数据库、API接口)的交互等方面。通过对这些内容的深入理解和学习,开发者可以更好地...
- SQL构造器:提供了流畅的API,使SQL构建更加直观。 - 自动映射:可以直接将数据库结果映射到Java对象,无需手动编写映射代码。 - 集成:支持多种数据库,包括MySQL、PostgreSQL、Oracle等。 二、JOOQ与Spring...