領域特定語言(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!皟炔緿SL”幾乎是一個特定領域內針對特定問題而創建的極具可讀性的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); 該范例并不解決任何領域特定問題。 除了方法鏈接外,靜態工廠方法(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 }
|