Java-Android-jwebee
          Java-Android-jwebee
          對(duì)IT人來說,要成為一個(gè)優(yōu)秀的技術(shù)型管理者,除了需要具備扎實(shí)的技術(shù)基礎(chǔ)之外,還應(yīng)該培養(yǎng)良好的人際關(guān)系能力、談判與溝通技能、客戶關(guān)系與咨詢技能、商業(yè)頭腦和財(cái)務(wù)技能以及創(chuàng)新意識(shí),此外還要有巧妙的激勵(lì)技巧和化解沖突與解決突發(fā)問題的能力.

          作者 Alex Ruiz and Jeff Bay 譯者 沙曉蘭 發(fā)布于 2008年3月12日 上午1時(shí)4分

          社區(qū)
          Java
          主題
          領(lǐng)域特定語言

          簡(jiǎn)介

          領(lǐng)域特定語言(DSL)通常被定義為一種特別針對(duì)某類特殊問題的計(jì)算機(jī)語言,它不打算解決其領(lǐng)域外的問題。對(duì)于DSL的正式研究已經(jīng)持續(xù)很多年,直到最近,在程序員試圖采用最易讀并且簡(jiǎn)煉的方法來解決他們的問題的時(shí)候,內(nèi)部DSL意外地被寫入程序中。近來,隨著關(guān)于Ruby和其他一些動(dòng)態(tài)語言的出現(xiàn),程序員對(duì)DSL的興趣越來越濃。這些結(jié)構(gòu)松散的語言給DSL提供某種方法,使得DSL允許最少的語法以及對(duì)某種特殊語言最直接的表現(xiàn)。但是,放棄編譯器和使用類似Eclipse這樣最強(qiáng)大的現(xiàn)代集成開發(fā)環(huán)境無疑是該方式的一大缺點(diǎn)。然而,作者終于成功地找到了這兩個(gè)方法的折衷解決方式,并且,他們將證明該折衷方法不但可能,而且對(duì)于使用Java這樣的結(jié)構(gòu)性語言從面向DSL的方式來設(shè)計(jì)API很有幫助。本文將描述怎樣使用Java語言來編寫領(lǐng)域特定語言,并將建議一些組建DSL語言時(shí)可采用的模式。

           

          Java適合用來創(chuàng)建內(nèi)部領(lǐng)域特定語言嗎?

          在我們審視Java語言是否可以作為創(chuàng)建DSL的工具之前,我們首先需要引進(jìn)“內(nèi)部DSL”這個(gè)概念。一個(gè)內(nèi)部DSL在由應(yīng)用軟件的主編程語言創(chuàng)建,對(duì)定制編譯器和解析器的創(chuàng)建(和維護(hù))都沒有任何要求。Martin Fowler曾編寫過大量各種類型的DSL,無論是內(nèi)部的還是外部的,每種類型他都編寫過一些不錯(cuò)的例子。但使用像Java這樣的語言來創(chuàng)建DSL,他卻僅僅一筆帶過。

          另外還要著重提出的很重要的一點(diǎn)是,在DSL和API兩者間其實(shí)很難區(qū)分。在內(nèi)部DSL的例子中,他們本質(zhì)上幾乎是一樣的。在聯(lián)想到DSL這個(gè)詞匯的時(shí)候,我們其實(shí)是在利用主編程語言在有限的范圍內(nèi)創(chuàng)建易讀的API。“內(nèi)部DSL”幾乎是一個(gè)特定領(lǐng)域內(nèi)針對(duì)特定問題而創(chuàng)建的極具可讀性的API的代名詞。

          任何內(nèi)部DSL都受它基礎(chǔ)語言的文法結(jié)構(gòu)的限制。比如在使用Java的情況下,大括弧,小括弧和分號(hào)的使用是必須的,并且缺少閉包和元編程有可能會(huì)導(dǎo)致DSL比使用動(dòng)態(tài)語言創(chuàng)建來的更冗長(zhǎng)。

          但從光明的一面來看,通過使用Java,我們同時(shí)能利用強(qiáng)大且成熟的類似于Eclipse和IntelliJ IDEA的集成開發(fā)環(huán)境,由于這些集成開發(fā)環(huán)境“自動(dòng)完成(auto-complete)”、自動(dòng)重構(gòu)和debug等特性,使得DSL的創(chuàng)建、使用和維護(hù)來的更加簡(jiǎn)單。另外,Java5中的一些新特性(比如generic、varargs 和static imports)可以幫助我們創(chuàng)建比以往任何版本任何語言都簡(jiǎn)潔的API。

          一般來說,使用Java編寫的DSL不會(huì)造就一門業(yè)務(wù)用戶可以上手的語言,而會(huì)是一種業(yè)務(wù)用戶也會(huì)覺得易讀的語言,同時(shí),從程序員的角度,它也會(huì)是一種閱讀和編寫都很直接的語言。和外部DSL或由動(dòng)態(tài)語言編寫的DSL相比有優(yōu)勢(shì),那就是編譯器可以增強(qiáng)糾錯(cuò)能力并標(biāo)識(shí)不合適的使用,而Ruby或Pearl會(huì)“愉快接受”荒謬的input并在運(yùn)行時(shí)失敗。這可以大大減少冗長(zhǎng)的測(cè)試,并極大地提高應(yīng)用程序的質(zhì)量。然而,以這樣的方式利用編譯器來提高質(zhì)量是一門藝術(shù),目前,很多程序員都在為盡力滿足編譯器而非利用它來創(chuàng)建一種使用語法來增強(qiáng)語義的語言。

          利用Java來創(chuàng)建DSL有利有弊。最終,你的業(yè)務(wù)需求和你所工作的環(huán)境將決定這個(gè)選擇正確與否。

          將Java作為內(nèi)部DSL的平臺(tái)

          動(dòng)態(tài)構(gòu)建SQL是一個(gè)很好的例子,其建造了一個(gè)DSL以適合SQL領(lǐng)域,獲得了引人注意的優(yōu)勢(shì)。

          傳統(tǒng)的使用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'";

          從作者最近工作的系統(tǒng)中摘錄的另一個(gè)表達(dá)方式是:

          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();

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

          使用Java創(chuàng)建內(nèi)部DSL

          創(chuàng)建DSL最好的方法是,首先將所需的API原型化,然后在基礎(chǔ)語言的約束下將它實(shí)現(xiàn)。DSL的實(shí)現(xiàn)將會(huì)牽涉到連續(xù)不斷的測(cè)試來肯定我們的開發(fā)確實(shí)瞄準(zhǔn)了正確的方向。該“原型-測(cè)試”方法正是測(cè)試驅(qū)動(dòng)開發(fā)模式(TDD-Test-Driven Development)所提倡的。

          在使用Java來創(chuàng)建DSL的時(shí)候,我們可能想通過一個(gè)連貫接口(fluent interface)來創(chuàng)建DSL。連貫接口可以對(duì)我們所想要建模的領(lǐng)域問題提供一個(gè)簡(jiǎn)介但易讀的表示。連貫接口的實(shí)現(xiàn)采用方法鏈接(method chaining)。但有一點(diǎn)很重要,方法鏈接本身不足以創(chuàng)建DSL。一個(gè)很好的例子是Java的StringBuilder,它的方法“append”總是返回一個(gè)同樣的StringBuilder的實(shí)例。這里有一個(gè)例子:

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

          該范例并不解決任何領(lǐng)域特定問題。

          除了方法鏈接外,靜態(tài)工廠方法(static factory method)和import對(duì)于創(chuàng)建簡(jiǎn)潔易讀的DSL來說是不錯(cuò)的助手。在下面的章節(jié)中,我們將更詳細(xì)地講到這些技術(shù)。

          1.方法鏈接(Method Chaining)

          使用方法鏈接來創(chuàng)建DSL有兩種方式,這兩種方式都涉及到鏈接中方法的返回值。我們的選擇是返回this或者返回一個(gè)中間對(duì)象,這決定于我們?cè)噲D要所達(dá)到的目的。

          1.1 返回this

          在可以以下列方式來調(diào)用鏈接中方法的時(shí)候,我們通常返回this

          • 可選擇的
          • 以任何次序調(diào)用
          • 可以調(diào)用任何次數(shù)

          我們發(fā)現(xiàn)運(yùn)用這個(gè)方法的兩個(gè)用例:

          1. 相關(guān)對(duì)象行為鏈接
          2. 一個(gè)對(duì)象的簡(jiǎn)單構(gòu)造/配置

          1.1.1 相關(guān)對(duì)象行為鏈接

          很多次,我們只在企圖減少代碼中不必要的文本時(shí),才通過模擬分派“多信息”(或多方法調(diào)用)給同一個(gè)對(duì)象而將對(duì)象的方法進(jìn)行鏈接。下面的代碼段顯示的是一個(gè)用來測(cè)試Swing GUI的API。測(cè)試所證實(shí)的是,如果一個(gè)用戶試圖不輸入她的密碼而登錄到系統(tǒng)中的話,系統(tǒng)將顯示一條錯(cuò)誤提示信息。

          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");

          盡管代碼很容易讀懂,但卻很冗長(zhǎng),需要很多鍵入。

          下面列出的是在我們范例中所使用的TextComponentFixture的兩個(gè)方法:

          public void clear() {
          target.setText("");
          }

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

          我們可以僅僅通過返回this來簡(jiǎn)化我們的測(cè)試API,從而激活方法鏈接:

          public TextComponentFixture clear() {
          target.setText("");
          return this;
          }

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

          在激活所有測(cè)試設(shè)施中的方法鏈接之后,我們的測(cè)試代碼現(xiàn)在縮減到:

          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");

          這個(gè)結(jié)果代碼顯然更加簡(jiǎn)潔易讀。正如先前所提到的,方法鏈接本身并不意味著有了DSL。我們需要將解決領(lǐng)域特定問題的對(duì)象的所有相關(guān)行為相對(duì)應(yīng)的方法鏈接起來。在我們的范例中,這個(gè)領(lǐng)域特定問題就是Swing GUI測(cè)試。

          1.1.2 對(duì)象的簡(jiǎn)單構(gòu)造/配置

          這個(gè)案例和上文的很相似,不同是,我們不再只將一個(gè)對(duì)象的相關(guān)方法鏈接起來,取而代之的是,我們會(huì)通過連貫接口創(chuàng)建一個(gè)“builder”來構(gòu)建和/或配置對(duì)象。

          下面這個(gè)例子采用了setter來創(chuàng)建“dream car”:

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

          DreamCar類的代碼相當(dāng)簡(jiǎn)單:

          // 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
          }

          盡管創(chuàng)建DreamCar非常簡(jiǎn)單,并且代碼也十分可讀,但我們?nèi)阅軌蚴褂胏ar builder來創(chuàng)造更簡(jiǎn)明的代碼:

          // package declaration and imports

          public class DreamCarBuilder {

          public static DreamCarBuilder car() {
          return new DreamCarBuilder();
          }

          private final DreamCar car;

          private DreamCarBuilder() {
          car = new DreamCar();
          }

          public DreamCar build() { return car; }

          public DreamCarBuilder brand(String brand) {
          car.setBrand(brand);
          return this;
          }

          public DreamCarBuilder fuelEfficient() {
          car.setFuelEfficient(true);
          return this;
          }

          // similar methods to set field values
          }

          通過builder,我們還能這樣重新編寫DreamCar的創(chuàng)建過程:

          DreamCar car = car().brand("Tesla")
          .color(RED)
          .fuelEfficient()
          .build();

          使用連貫接口,再一次減少了代碼噪音,所帶來的結(jié)果是更易讀的代碼。需要指出的很重要的一點(diǎn)是,在返回this的時(shí)候,鏈中任何方法都可以在任何時(shí)候被調(diào)用,并且可以被調(diào)用任何次數(shù)。在我們的例子中,color這個(gè)方法我們可想調(diào)用多少次就調(diào)用多少次,并且每次調(diào)用都會(huì)覆蓋上一次調(diào)用所設(shè)置的值,這在應(yīng)用程序的上下文中可能是合理的。

          另一個(gè)重要的發(fā)現(xiàn)是,沒有編譯器檢查來強(qiáng)制必需的屬性值。一個(gè)可能的解決方案是,如果任何對(duì)象創(chuàng)建和/或配置規(guī)則沒有得到滿足的話(比如,一個(gè)必需屬性被遺忘),在運(yùn)行時(shí)拋出異常。通過從鏈中方法返回中間對(duì)象有可能達(dá)到規(guī)則校驗(yàn)的目的。

          1.2 返回中間對(duì)象

          從連貫接口的方法中返回中間對(duì)象和返回this的方式相比,有這樣一些優(yōu)點(diǎn):

          • 我們可以使用編譯器來強(qiáng)制業(yè)務(wù)規(guī)則(比如:必需屬性)
          • 我們可以通過限制鏈中下一個(gè)元素的可用選項(xiàng),通過一個(gè)特殊途徑引導(dǎo)我們的連貫接口用戶
          • 在用戶可以(或必須)調(diào)用哪些方法、調(diào)用順序、用戶可以調(diào)用多少次等方面,給了API創(chuàng)建者更大的控制力

          下面的例子表示的是通過帶參數(shù)的構(gòu)建函數(shù)來創(chuàng)建一個(gè)vacation對(duì)象的實(shí)例:

          Vacation vacation = new Vacation("10/09/2007", "10/17/2007",
          "Paris", "Hilton",
          "United", "UA-6886");

          這個(gè)方法的好處在于它可以迫使我們的用戶申明所有必需的參數(shù)。不幸的是,這兒有太多的參數(shù),而且沒有表達(dá)出他們的目的。“Paris”和“Hilton”所指的分別是目的地的城市和酒店?還是我們同事的名字?:)

          第二個(gè)方法是將setter方法對(duì)每個(gè)參數(shù)進(jìn)行建檔:

          Vacation vacation = new Vacation();
          vacation.setStart("10/09/2007");
          vacation.setEnd("10/17/2007");
          vacation.setCity("Paris");
          vacation.setHotel("Hilton");
          vacation.setAirline("United");
          vacation.setFlight("UA-6886");

          現(xiàn)在我們的代碼更易讀,但仍然很冗長(zhǎng)。第三個(gè)方案則是創(chuàng)建一個(gè)連貫接口來構(gòu)建vacation對(duì)象的實(shí)例,如同在前一章節(jié)提供的例子一樣:

          Vacation vacation = vacation().starting("10/09/2007")
          .ending("10/17/2007")
          .city("Paris")
          .hotel("Hilton")
          .airline("United")
          .flight("UA-6886");

          這個(gè)版本的簡(jiǎn)明和可讀性又進(jìn)了一步,但我們丟失了在第一個(gè)版本(使用構(gòu)建函數(shù)的那個(gè)版本)中所擁有的關(guān)于遺忘屬性的校驗(yàn)。換句話說,我們并沒有使用編譯器來校驗(yàn)可能存在的錯(cuò)誤。這時(shí),對(duì)這個(gè)方法我們所能做的最好的改進(jìn)是,如果某個(gè)必需屬性沒有設(shè)置的話,在運(yùn)行時(shí)拋出異常。

          以下是第四個(gè)版本,連貫接口更完善的版本。這次,方法返回的是中間對(duì)象,而不是this:

          Period vacation = from("10/09/2007").to("10/17/2007");
          Booking booking = vacation.book(city("Paris").hotel("Hilton"));
          booking.add(airline("united").flight("UA-6886");

          這里,我們引進(jìn)了PeriodBookingLocationBookableItemHotelFlight)、以及 Airline的概念。在這里的上下文中,airline作為Flight對(duì)象的一個(gè)工廠;LocationHotel的工廠,等等。我們所想要的booking的文法隱含了所有這些對(duì)象,幾乎可以肯定的是,這些對(duì)象在系統(tǒng)中會(huì)有許多其他重要的行為。采用中間對(duì)象,使得我們可以對(duì)用戶行為可否的限制進(jìn)行編譯器校驗(yàn)。例如,如果一個(gè)API的用戶試圖只通過提供一個(gè)開始日期而沒有明確結(jié)束日期來預(yù)定假期的話,代碼則不會(huì)被編譯。正如我們之前提到,我們可以創(chuàng)建一種使用文法來增強(qiáng)語義的語言。

          我們?cè)谏厦娴睦又羞€引入了靜態(tài)工廠方法的應(yīng)用。靜態(tài)工廠方法在與靜態(tài)import同時(shí)使用的時(shí)候,可以幫助我們創(chuàng)建更簡(jiǎn)潔的連貫接口。若沒有靜態(tài)import,上面的例子則需要這樣的代碼:

          Period vacation = Period.from("10/09/2007").to("10/17/2007");
          Booking booking = vacation.book(Location.city("Paris").hotel("Hilton"));
          booking.add(Flight.airline("united").flight("UA-6886");

          上面的例子不及采用了靜態(tài)import的代碼那么易讀。在下面的章節(jié)中,我們將對(duì)靜態(tài)工廠方法和import做更詳細(xì)的講解。

          這是關(guān)于使用Java編寫DSL的第二個(gè)例子。這次,我們將Java reflection的使用進(jìn)行簡(jiǎn)化:

          Person person = constructor().withParameterTypes(String.class)
          .in(Person.class)
          .newInstance("Yoda");

          method("setName").withParameterTypes(String.class)
          .in(person)
          .invoke("Luke");

          field("name").ofType(String.class)
          .in(person)
          .set("Anakin");

          在使用方法鏈接的時(shí)候,我們必須倍加注意。方法鏈接很容易會(huì)被爛用,它會(huì)導(dǎo)致許多調(diào)用被一起鏈接在單一行中的“火車殘骸”現(xiàn)象。這會(huì)引發(fā)很多問題,包括可讀性的急劇下滑以及異常發(fā)生時(shí)棧軌跡(stack trace)的含義模糊。

          2. 靜態(tài)工廠方法和Imports

          靜態(tài)工廠方法和imports可以使得API更加簡(jiǎn)潔易讀。我們發(fā)現(xiàn),靜態(tài)工廠方法是在Java中模擬命名參數(shù)的一個(gè)非常方便的方法,是許多程序員希望開發(fā)語言中所能夠包含的特性。比如,對(duì)于這樣一段代碼,它的目的在于通過模擬一個(gè)用戶在一個(gè)JTable中選擇一行來測(cè)試GUI:

          dialog.table("results").selectCell(6, 8); // row 6, column 8 

          沒有注釋“// row 6, column 8”,這段代碼想要實(shí)現(xiàn)的目的很容易被誤解(或者說根本沒有辦法理解)。我們則需要花一些額外的時(shí)間來檢查文檔或者閱讀更多行代碼才能理解“6”和“8”分別代表什么。我們也可以將行和列的下標(biāo)作為變量來聲明,而非像上面這段代碼那樣使用常量:

          int row = 6;
          int column = 8;
          dialog.table("results").selectCell(row, column);

          我們已經(jīng)改進(jìn)了這段代碼的可讀性,但卻付出了增加需要維護(hù)的代碼的代價(jià)。為了將代碼盡量簡(jiǎn)化,理想的解決方案是像這樣編寫代碼:

          dialog.table("results").selectCell(row: 6, column: 8); 

          不幸的是,我們不能這樣做,因?yàn)镴ava不支持命名參數(shù)。好的一面的是,我們可以通過使用靜態(tài)工廠方法和靜態(tài)imports來模擬他們,從而可以得到這樣的代碼:

          dialog.table("results").selectCell(row(6).column(8)); 

          我們可以從改變方法的簽名(signature)開始,通過包含所有參數(shù)的對(duì)象來替代所有這些參數(shù)。在我們的例子中,我們可以將方法selectCell(int, int)修改為:

          selectCell(TableCell); 

          TableCell will contain the values for the row and column indices:

          TableCell將包含行和列的下標(biāo)值:

          public final class TableCell {

          public final int row;
          public final int column;

          public TableCell(int row, int column) {
          this.row = row;
          this.column = column;
          }
          }

          這時(shí),我們只是將問題轉(zhuǎn)移到了別處:TableCell的構(gòu)造函數(shù)仍然需要兩個(gè)int值。下一步則是將引入一個(gè)TableCell的工廠,這個(gè)工廠將對(duì)初始版本中selectCell的每個(gè)參數(shù)設(shè)置一個(gè)對(duì)應(yīng)的方法。另外,為了迫使用戶使用工廠,我們需要將TableCell的構(gòu)建函數(shù)修改為private

          public final class TableCell {

          public static class TableCellBuilder {
          private final int row;

          public TableCellBuilder(int row) {
          this.row = row;
          }

          public TableCell column(int column) {
          return new TableCell(row, column);
          }
          }

          public final int row;
          public final int column;

          private TableCell(int row, int column) {
          this.row = row;
          this.column = column;
          }
          }

          通過TableCellBuilder工廠,我們可以創(chuàng)建對(duì)每個(gè)參數(shù)都有一個(gè)調(diào)用方法的TableCell。工廠中的每個(gè)方法都表達(dá)了其參數(shù)的目的:

          selectCell(new TableCellBuilder(6).column(8)); 

          最后一步是引進(jìn)靜態(tài)工廠方法來替代TableCellBuilder構(gòu)造函數(shù)的使用,該構(gòu)造函數(shù)沒有表達(dá)出6代表的是什么。如我們?cè)谥八鶎?shí)現(xiàn)的那樣,我們需要將構(gòu)造函數(shù)設(shè)置為private來迫使用戶使用工廠方法:

          public final class TableCell {

          public static class TableCellBuilder {
          public static TableCellBuilder row(int row) {
          return new TableCellBuilder(row);
          }

          private final int row;

          private TableCellBuilder(int row) {
          this.row = row;
          }

          private TableCell column(int column) {
          return new TableCell(row, column);
          }
          }

          public final int row;
          public final int column;

          private TableCell(int row, int column) {
          this.row = row;
          this.column = column;
          }
          }

          現(xiàn)在我們只需要selectCell的調(diào)用代碼中增加內(nèi)容,包含對(duì)TableCellBuilderrow方法的靜態(tài)import。為了刷新一下我們的記憶,這是如何實(shí)現(xiàn)調(diào)用selectCell的代碼:

          dialog.table("results").selectCell(row(6).column(8)); 

          我們的例子說明,一點(diǎn)點(diǎn)額外的工作可以幫助我們克服主機(jī)編程語言中的一些限制。正如之前提到的,這只是我們通過使用靜態(tài)工廠方法和imports來改善代碼可讀性的很多方法中的一個(gè)。下列代碼段是以另一種不同的方法利用靜態(tài)工廠方法和imports來解決相同的table坐標(biāo)問題:

          /**
          * @author Mark Alexandre
          */
          public final class TableCellIndex {

          public static final class RowIndex {
          final int row;
          RowIndex(int row) {
          this.row = row;
          }
          }

          public static final class ColumnIndex {
          final int column;
          ColumnIndex(int column) {
          this.column = column;
          }
          }

          public final int row;
          public final int column;
          private TableCellIndex(RowIndex rowIndex, ColumnIndex columnIndex) {
          this.row = rowIndex.row;
          this.column = columnIndex.column;
          }

          public static TableCellIndex cellAt(RowIndex row, ColumnIndex column) {
          return new TableCellIndex(row, column);
          }

          public static TableCellIndex cellAt(ColumnIndex column, RowIndex row) {
          return new TableCellIndex(row, column);
          }

          public static RowIndex row(int index) {
          return new RowIndex(index);
          }

          public static ColumnIndex column(int index) {
          return new ColumnIndex(index);
          }
          }

          這個(gè)方案的第二個(gè)版本比第一個(gè)版本更具靈活性,因?yàn)檫@個(gè)版本允許我們通過兩種途徑來聲明行和列的坐標(biāo):

          dialog.table("results").select(cellAt(row(6), column(8));
          dialog.table("results").select(cellAt(column(3), row(5));

          組織代碼

          相比返回中間對(duì)象的的方式來說,返回this的方式更加容易組織連貫接口的代碼。前面的案例中,我們的最后結(jié)果是使用更少的類來封裝連貫接口的邏輯,并且使得我們可以在組織非DSL代碼的時(shí)候使用同樣的規(guī)則或約定。

          采用中間對(duì)象作為返回類型來組織連貫接口的代碼更具技巧性,因?yàn)槲覀儗⑦B貫接口的邏輯遍布在一些小的類上。由于這些類結(jié)合在一起作為整體而形成我們的連貫接口,這使得將他們作為整體對(duì)待更為合理,我們可能不想將他們和DSL外的其他一些類混淆一起,那么我們有兩個(gè)選擇:

          • 將中間對(duì)象作為內(nèi)嵌類創(chuàng)建
          • 將中間對(duì)象至于他們自己的頂級(jí)類中,將所有這些中間對(duì)象類放入同一個(gè)包中

          分解我們的系統(tǒng)所采用的方式取決于我們想要實(shí)現(xiàn)的文法的幾個(gè)因素:DSL的目的,中間對(duì)象(如果有的話)的數(shù)量和大小(以代碼的行數(shù)來計(jì)),以及DSL如何來與其它的代碼庫及其它的DSL相協(xié)調(diào)。

          對(duì)代碼建檔

          在組織代碼一章節(jié)中提到,對(duì)方法返回this的連貫接口建檔比對(duì)返回中間對(duì)象的連貫接口建檔來的簡(jiǎn)單的多,尤其是在使用Javadoc來建檔的情況下。

          Javadoc每次顯示一個(gè)類的文檔,這對(duì)于使用中間對(duì)象的DSL來說可能不是最好的方式:因?yàn)檫@樣的DSL包含一組類,而不是單個(gè)的類。由于我們不能改變Javadoc顯示我們的API文檔的方式,我們發(fā)現(xiàn)在package.html文件中,加入一個(gè)使用連貫接口(包含所有相關(guān)類)、且對(duì)鏈中每個(gè)方法提供鏈接的例子,可以將Javadoc的限制的影響降到最低。

          我們需要注意不要?jiǎng)?chuàng)建重復(fù)文檔,因?yàn)槟菢訒?huì)增加API創(chuàng)建者的維護(hù)代價(jià)。最好的方法是盡可能依賴于像可執(zhí)行文檔那樣的測(cè)試。

          結(jié)論

          Java適用于創(chuàng)建開發(fā)人員易讀易寫的、并且對(duì)于商業(yè)用戶用樣易讀的內(nèi)部領(lǐng)域特定語言。用Java創(chuàng)建的DSL可能比那些由動(dòng)態(tài)語言創(chuàng)建的DSL來的冗長(zhǎng)。但好的一面是,通過使用Java,我們可以利用編譯器來增強(qiáng)DSL的語義。另外,我們依賴于成熟且強(qiáng)大的Java集成開發(fā)環(huán)境,從而使DSL的創(chuàng)建、使用和維護(hù)更加簡(jiǎn)單。

          使用Java創(chuàng)建DSL需要API設(shè)計(jì)者做更多的工作,有更多的代碼和文檔需要?jiǎng)?chuàng)建和維護(hù)。但是,付出總有回報(bào)。使用我們API的用戶在他們的代碼庫中會(huì)看到更多的優(yōu)化。他們的代碼將會(huì)更加簡(jiǎn)潔,更易于維護(hù),這些將使得他們的生活更加輕松。

          使用Java創(chuàng)建DSL有很多種不同的方式,這取決于我們?cè)噲D達(dá)到的目的是什么。盡管沒有什么通用的方法,我們還是發(fā)現(xiàn)結(jié)合方法鏈接和靜態(tài)工廠方法與imports的方式可以得到干凈、簡(jiǎn)潔、易讀易寫的API。

          總而言之,在使用Java來創(chuàng)建DSL的時(shí)候有利有弊。這都由我們——開發(fā)人員根據(jù)項(xiàng)目需求去決定它是否是正確的選擇。

          另外一點(diǎn)題外話,Java 7可能會(huì)包含幫助我們創(chuàng)建不那么冗長(zhǎng)的DSL的新語言特性(比如閉包)。如果想得到更多關(guān)于建議中所提特性的全面的列表,請(qǐng)?jiān)L問Alex Miller的blog

          關(guān)于作者

          Alex Ruiz是Oracle開發(fā)工具組織中的一名軟件工程師。Alex喜歡閱讀任何關(guān)于Java、測(cè)試、OOP 和AOP的信息,他最大的愛好就是編程。在加入Oracle之前,Alex曾是ThoughtWorks的咨詢顧問。Alex的blog為 http://www.jroller.com/page/alexRuiz

          Jeff Bay是紐約一家對(duì)沖基金的高級(jí)軟件工程師。他曾多次建立高質(zhì)量、迅速的XP團(tuán)隊(duì)工作于例如Onstar的計(jì)劃注冊(cè)系統(tǒng)、租賃軟件、web服務(wù)器、建筑項(xiàng)目管理等各種系統(tǒng)。他對(duì)于消除重復(fù)和防止bug方面懷有極大的熱情,以提高開發(fā)者的工作效率和減少在各種任務(wù)上所花費(fèi)的時(shí)間。



          jwebee

          我的個(gè)人網(wǎng)站
          posted on 2008-03-17 19:03 周行 閱讀(350) 評(píng)論(0)  編輯  收藏 所屬分類: IT技術(shù)
          Java-Android-jwebee
          主站蜘蛛池模板: 新晃| 黄陵县| 慈溪市| 宁波市| 根河市| 珲春市| 河北省| 洛阳市| 凤城市| 会同县| 黎平县| 呼伦贝尔市| 崇左市| 天柱县| 明星| 威宁| 游戏| 伊宁县| 东乌珠穆沁旗| 邵阳县| 盘锦市| 五家渠市| 鸡东县| 元江| 资源县| 莒南县| 七台河市| 读书| 铜陵市| 杭锦旗| 咸宁市| 南涧| 尤溪县| 凤凰县| 航空| 阿鲁科尔沁旗| 金平| 广灵县| 清流县| 博爱县| 陆川县|