API规划新思惟:用流利接口构造外部DSL
措施规划措辞的笼统机制包括了两个最根本的方面:一是措辞存眷的根本元素/语义;另外一个是从根本元素/语义到复合元素/语义的构造法则。在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个数,我们一样平时会首先想到近似多么的代码:
- //Java
- for (int i = 0; i < 5; ++i) {
- system.out.println(i);
- }
-
而Ruby虽然也支持近似的for轮回,但最简略的是下面多么的完成:
- //Ruby
- .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的“用两个时候点构造一个时候段工具”,平凡规划:
- 3 //Java
- TimePoint fiveOClock, sixOClock;
- TimeInterval meetingTime = new TimeInterval(fiveOClock, sixOClock);
-
另外一种Evans的规划是多么:
- 2 //Java
- TimeInterval meetingTime = fiveOClock.until(sixOClock);
按传统OO规划,until体例本不应泛起在TimePoint类中,这里TimePoint类的until体例一样代表了一种自定义的根本语义,使得表达时候域的结果更加自然。
虽然下面的两个简略例子和平凡规划比较看不出太大的优势,但它却为我们理解流利接口打下了根本。重要的是应该领会到它们从必定程度上跳出了措辞根本笼统机制的束厄局促,我们不应该再用类职责分别、迪米特法例(Law of Demeter)等OO规划绳尺来看待它们。
2.管道笼统
在Shell中,我们可以大概经由过程管道将一系列的小号令组合在一起完成庞杂的服从。管道中活动的是单一规范的文本流,计较历程就是从输入流到输入流的变动历程,每一个号令是对文本流的一次变动浸染,经由过程管道将浸染叠加起来。在Shell中,很多时辰我们只需要一句话就能完成log统计多么的中小局限结果。和其他笼统机制比较,管道的美好在于无嵌套。比以下面这段C措施,因为嵌套层次较深,不随便马虎一会儿理解清楚:
- 2 //C
- min(max(min(max(a,b),c),d),e)
而用管道来表达一样的服从则清楚很多:
-
- 2 #!/bin/bash
- max a b | min c | max d | min e
-
我们很随便马虎理解这段措施表达的意义是:先求a,b的最大值;再把结果和c取最小值;再把结果和d求最大值;再把结果和e求最小值。
jQuery的链式调用规划也具有管道的作风,体例链上活动的是同一规范的jQuery工具,每步体例调用是对工具的一次浸染,全部体例链将各个体例的浸染叠加起来。
- 2 //Javascript
- $('li').filter(':event').css('background-color', 'red');
-
3.层次布局笼统
除管道这类“线性”布局外,流利接口还可用于构造层次布局笼统。比如,用Javascript静态建立建立下面的HTML片断:
- <div id="’product_123’" class="’product’">
- <img src="’preview_123.jpg’" alt="" />
- <ul>
- <li>Name: iPad2 32Gli>
- <li>Price: 3600li>
- ul>
- div>
-
若回收Javascript的DOM API:
- //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则要有浮现,力很多:
- //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();
和Javascript的尺度DOM API比较,下面的API规划不再局限于伶仃地看待某一个体例,而是斟酌了它们在打点结果时的组合利用,以是代码的浮现,方式特别切近结果的实质。多么的代码是自诠释的(self-explanatory)在可读性方面要较着胜于DOM API,这相等于定义了一种近似于HTML的外部DSL,它具有自己的语义和语法。需要特别留意的是,下面的层次布局笼统和管道笼统有着实质的不合,管道笼统的体例链上经常是同一工具的陆续通报,而层次笼统中体例链上的工具却在随着层次的改变而改变。此为,我们可以大概把业务法则也表达在流利接口中,比如下面的例子中,body()不克不及包括在div()前去的工具中,div().body()将抛出”body体例不存在”非常。(高端网站设立建设)
4.异步笼统
流利接口不只可以大概构造庞杂的层次笼统,还可以大概用于构造异步笼统。在基于回调机制的异步方式中,多个异步调用的同步和嵌套结果是利用异步的难点地址。有时一个稍庞杂的调用和同步关系会致使代码充斥了庞杂的同步检讨和层层回调,难以理解和维护。这个结果从实质上媾和下面HTML的例子一样,是因为多数通用措辞并未把异步作为根本元素/语义,很多异步完成方式是向措辞的让步。针对这个结果,我用Javascript编写了一个基于流利接口的异步DSL,示例代码以下:
- //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布局自己是可以大概嵌套的,比如下面的代码在timeout措置分支中就包括了另外一个begin when end布局。经由过程这个DSL,我们可以大概比基于回调的体例更好地表达异步调用的同步和嵌套关系。
下面先容了用流利接口构造的4种典范笼统,出此之外另有很多其他的笼统和利用场所,比如:很多单元测试框架就经由过程流利接口定义了单元测试的DSL。虽然下面的例子以Javascript等静态措辞占多数,但其实流利接口所依托的语法根本其实不刻薄,即使在Java多么的静态措辞中,一样可以大概轻松地利用。流利接口不合于传统的API规划,理解和利用流利接口环节是要冲破措辞笼统机制带来的定势思惟,凭据结果域拔取得当的笼统维度,把持措辞的根本语法构造范畴特定的语义和语法。