API設計新思維:用流暢接口構造內部DSL
編程語言的抽象機制包括兩個基本方面:一是語言關注的基本元素/語義;另一種是從基本元素/語義到復合元素/語義的構造規則。在C、C++、Java、C#、Python等通用語言中,語言的基本元素/語義往往遠離問題域。以API庫的形式逐層抽象是最常用的降低問題難度的方法。比如C語言中最常見的方式就是提供一個函數庫來封裝復雜的邏輯,方便外部調用。 (北京網出品)
但是,普通的API設計方法有一個天然的陷阱,就是不管怎么封裝,大進程的抽象層次雖然比小進程高,但本質上還是一個進程,是受流程語義的限制。也就是說,當通過基本元素/語義構造更高層次的抽象元素/語義時,語言的構造規則在很大程度上限制了抽象的維度,我們很難跳出這個維度,我們可能不會甚至完全意識到這個限制。但是,DSL(Domain Specific Language)的抽象維度,如SQL、HTML、CSS、make,都是為特定領域量身定做的。從這些抽象的角度來看,問題往往是最簡單的,所以在解決特定領域的問題時,DSL 比通用程序要好。設計語言更方便。通常將SQL等非通用語言稱為外部DSL(External DSL);在通用語言中,我們其實可以在一定程度上突破語言構造規則的抽象維度限制,定義內部DSL(Internal DSL)。
本文將介紹一種稱為流式接口(Fluent Interface)的內部DSL設計方法。維基百科上對Fluent Interface 的定義是:
流暢的接口(最初由Eric Evans 和Martin Fowler 創造)是面向對象API 的實現,旨在提供更具可讀性的代碼。流暢的接口通常是通過使用方法鏈接來中繼后續調用的指令上下文來實現的(但流暢的接口需要的不僅僅是方法鏈接)。
下面將分為四個部分逐步說明Fluent Interface在內部DSL構建中的典型應用。
1.基本語義抽象
如果我們要輸出5個數字0.4,我們通常首先想到的代碼是這樣的:
//Java
for(inti=0;i5;++i){
系統.out.println(i);
}
ss="alt">?而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?32G</li>?
- <li>Price:?3600</li>?
- </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設計,理解和使用流暢接口關鍵是要突破語言抽象機制帶來的定勢思維,根據問題域選取適當的抽象維度,利用語言的基本語法構造領域特定的語義和語法。
我們專注高端建站,小程序開發、軟件系統定制開發、BUG修復、物聯網開發、各類API接口對接開發等。十余年開發經驗,每一個項目承諾做到滿意為止,多一次對比,一定讓您多一份收獲!