使用Java實現內部領特定領域語言

來源:互聯網
上載者:User

http://bbs.dev.ccidnet.com/read.php?tid=586331

 

簡介

領特定領域語言(DSL)通常被定義為一種特別針對某類特殊問題的電腦語言,它不打算解決其領域外的問題。對於DSL的正式研究已經持續很多年,直到最近,在程式員試圖採用最易讀並且簡煉的方法來解決他們的問題的時候,內部DSL意外地被寫入程式中。近來,隨著關於Ruby和其他一些動態語言的出現,程式員對DSL的興趣越來越濃。這些結構鬆散的語言給DSL提供某種方法,使得DSL允許最少的文法以及對某種特殊語言最直接的表現。但是,放棄編譯器和使用類似Eclipse這樣最強大的現代整合式開發環境無疑是該方式的一大缺點。然而,作者終於成功地找到了這兩個方法的折衷解決方式,並且,他們將證明該折衷方法不但可能,而且對於使用Java這樣的結構性語言從面向DSL的方式來設計API很有協助。本文將描述怎樣使用Java語言來編寫領特定領域語言,並將建議一些組建DSL語言時可採用的模式。

Java適合用來建立內部領特定領域語言嗎?

在我們審視Java語言是否可以作為建立DSL的工具之前,我們首先需要引進“內部DSL”這個概念。一個內部DSL在由應用軟體的主程式設計語言建立,對定製編譯器和解析器的建立(和維護)都沒有任何要求。Martin Fowler曾編寫過大量各種類型的DSL,無論是內部的還是外部的,每種類型他都編寫過一些不錯的例子。但使用像Java這樣的語言來建立DSL,他卻僅僅一筆帶過。

另外還要著重提出的很重要的一點是,在DSL和API兩者間其實很難區分。在內部DSL的例子中,他們本質上幾乎是一樣的。在聯想到DSL這個詞彙的時候,我們其實是在利用主程式設計語言在有限的範圍內建立易讀的API。“內部DSL”幾乎是一個特定領域內針對特定問題而建立的極具可讀性的API的代名詞。

任何內部DSL都受它基礎語言的文法結構的限制。比如在使用Java的情況下,大括弧,小括弧和分號的使用是必須的,並且缺少閉包和元編程有可能會導致DSL比使用動態語言建立來的更冗長。

但從光明的一面來看,通過使用Java,我們同時能利用強大且成熟的類似於Eclipse和IntelliJ IDEA的整合式開發環境,由於這些整合式開發環境“自動完成(auto-complete)”、自動重構和debug等特性,使得DSL的建立、使用和維護來的更加簡單。另外,Java5中的一些新特性(比如generic、varargs 和static imports)可以協助我們建立比以往任何版本任何語言都簡潔的API。

一般來說,使用Java編寫的DSL不會造就一門業務使用者可以上手的語言,而會是一種業務使用者也會覺得易讀的語言,同時,從程式員的角度,它也會是一種閱讀和編寫都很直接的語言。和外部DSL或由動態語言編寫的DSL相比有優勢,那就是編譯器可以增強錯誤修正能力並標識不合適的使用,而Ruby或Pearl會“愉快接受”荒謬的input並在運行時失敗。這可以大大減少冗長的測試,並極大地提高應用程式的品質。然而,以這樣的方式利用編譯器來提高品質是一門藝術,目前,很多程式員都在為儘力滿足編譯器而非利用它來建立一種使用文法來增強語義的語言。
利用Java來建立DSL有利有弊。最終,你的業務需求和你所工作的環境將決定這個選擇正確與否。

將Java作為內部DSL的平台

動態構建SQL是一個很好的例子,其建造了一個DSL以適合SQL領域,獲得了引人注意的優勢。
傳統的使用SQL的Java代碼一般類似於:

String sql = "select id, name " +
"from customers c, order o " +
"where " +
"c.since >= sysdate - 30 and " +
"sum(o.total) > " + significantTotal + " and " +
"c.id = o.customer_id and " +
"nvl(c.status, 'DROPPED') != 'DROPPED'"; 

從作者最近工作的系統中摘錄的另一個表達方式是:

Table c = CUSTOMER.alias();
Table o = ORDER.alias();
Clause recent = c.SINCE.laterThan(daysEarlier(30));
Clause hasSignificantOrders = o.TOTAT.sum().isAbove(significantTotal);
Clause ordersMatch = c.ID.matches(o.CUSTOMER_ID);
Clause activeCustomer = c.STATUS.isNotNullOr("DROPPED");
String sql = CUSTOMERS.where(recent.and(hasSignificantOrders)
.and(ordersMatch)
.and(activeCustomer)
.select(c.ID, c.NAME)
.sql(); 

這個DSL版本有幾項優點。後者能夠透明地適應轉換到使用PreparedStatement的方法——用String拼字SQL的版本則需要大量的修改才能適應轉換到使用捆綁變數的方法。如果引用不正確或者一個integer變數被傳遞到date column作比較的話,後者版本根本無法通過編譯。代碼“nvl(foo, 'X') != 'X'”是Oracle SQL中的一種特殊形式,這個句型對於非Oracle SQL程式員或不熟悉SQL的人來說很難讀懂。例如在SQL Server方言中,該代碼應該這樣表達“(foo is null or foo != 'X')”。但通過使用更易理解、更像人類語言的“isNotNullOr(rejectedValue)”來替代這段代碼的話,顯然會更具閱讀性,並且系統也能夠受到保護,從而避免將來為了利用另一個資料庫供應商的設施而不得不修改最初的代碼實現。

使用Java建立內部DSL

建立DSL最好的方法是,首先將所需的API原型化,然後在基礎語言的約束下將它實現。DSL的實現將會牽涉到連續不斷的測試來肯定我們的開發確實瞄準了正確的方向。該“原型-測試”方法正是測試驅動開發模式(TDD-Test-Driven Development)所提倡的。

在使用Java來建立DSL的時候,我們可能想通過一個連貫介面(fluent interface)來建立DSL。連貫介面可以對我們所想要建模的領域問題提供一個簡介但易讀的表示。連貫介面的實現採用方法連結(method chaining)。但有一點很重要,方法連結本身不足以建立DSL。一個很好的例子是Java的StringBuilder,它的方法“append”總是返回一個同樣的StringBuilder的執行個體。這裡有一個例子:

StringBuilder b = new StringBuilder();
b.append("Hello. My name is ")
.append(name)
.append(" and my age is ")
.append(age); 

該範例並不解決任何領域特定問題。

除了方法連結外,靜態Factory 方法(static factory method)和import對於建立簡潔易讀的DSL來說是不錯的助手。在下面的章節中,我們將更詳細地講到這些技術。

1、方法連結(Method Chaining)

使用方法連結來建立DSL有兩種方式,這兩種方式都涉及到連結中方法的傳回值。我們的選擇是返回this或者返回一個中間對象,這決定於我們試圖要所達到的目的。

1.1、返回this

在可以以下列方式來調用連結中方法的時候,我們通常返回this:

◆可選擇的。
◆以任何次序調用。
◆可以調用任何次數。

我們發現運用這個方法的兩個用例:

1、相關對象行為連結。
2、一個對象的簡單構造/配置。

1.1.1、相關對象行為連結

很多次,我們只在企圖減少代碼中不必要的文本時,才通過類比指派“多資訊”(或多方法調用)給同一個對象而將對象的方法進行連結。下面的程式碼片段顯示的是一個用來測試Swing GUI的API。測試所證實的是,如果一個使用者試圖不輸入她的密碼而登入到系統中的話,系統將顯示一條錯誤提示資訊。

DialogFixture dialog = new DialogFixture(new LoginDialog());
dialog.show();
dialog.maximize();
TextComponentFixture usernameTextBox = dialog.textBox("username");
usernameTextBox.clear();
usernameTextBox.enter("leia.organa");
dialog.comboBox("role").select("REBEL");
OptionPaneFixture errorDialog = dialog.optionPane();
errorDialog.requireError();
errorDialog.requireMessage("Enter your password"); 

儘管代碼很容易讀懂,但卻很冗長,需要很多鍵入。

下面列出的是在我們範例中所使用的TextComponentFixture的兩個方法:

public void clear() {
target.setText("");
}
public void enterText(String text) {
robot.enterText(target, text);

我們可以僅僅通過返回this來簡化我們的測試API,從而啟用方法連結:

public TextComponentFixture clear() {
target.setText("");
return this;
}
public TextComponentFixture enterText(String text) {
robot.enterText(target, text);
return this;

在啟用所有測試設施中的方法連結之後,我們的測試代碼現在縮減到:

DialogFixture dialog = new DialogFixture(new LoginDialog());
dialog.show().maximize();
dialog.textBox("username").clear().enter("leia.organa");
dialog.comboBox("role").select("REBEL");
dialog.optionPane().requireError().requireMessage("Enter your password");

這個結果代碼顯然更加簡潔易讀。正如先前所提到的,方法連結本身並不意味著有了DSL。我們需要將解決領域特定問題的對象的所有相關行為相對應的方法連結起來。在我們的範例中,這個領域特定問題就是Swing GUI測試。

1.1.2、對象的簡單構造/配置

這個案例和上文的很相似,不同是,我們不再只將一個對象的相關方法連結起來,取而代之的是,我們會通過連貫介面建立一個“builder”來構建和/或設定物件。

下面這個例子採用了setter來建立“dream car”:

DreamCar car = new DreamCar();
car.setColor(RED);
car.setFuelEfficient(true);
car.setBrand("Tesla"); 

DreamCar類的代碼相當簡單:

// package declaration and imports
public class DreamCar {
private Color color;
private String brand;
private boolean leatherSeats;
private boolean fuelEfficient;
private int passengerCount = 2;
// getters and setters for each field

更多請見…………http://java.chinaitlab.com/base/741255_2.html

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.