程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Groovy >> 演化架構和緊急設計 - 使用Groovy構建DSL

演化架構和緊急設計 - 使用Groovy構建DSL

編輯:Groovy

在 上個月的這一專欄 中,我講述了使用特定領域語言(DSL)的示例,在您的代碼中定義為通用設計習慣。(我在 “組合方法和 SLAP” 一文中介紹了慣用模式的概念。)DSL 是捕獲模式的一個良好介質,因為它們是聲明式的,比 “普通” 源代碼更容易閱讀,使您的捕獲模式從周圍的代碼中脫穎而出。

構建 DSL 的語言技術通常使用巧妙的方法來為您的代碼隱式地提供包裝上下文。換句話說,DSL 試圖使用潛在語言特性 “隱藏” 雜亂的語法來使您的代碼更具可讀性。盡管您可以使用 Java 語言構建 DSL,但是 DSL 用於隱藏上下文的貧乏的構造,及其死板和無常的語法,使它不適合這一技術。但是其他基於 JVM 的語言可以填補這一空缺。在本期以及下一期中,我將向您介紹如何擴展您的 DSL 構建調板,包含更富於表現力的在 Java 平台上運行的語言,從 Groovy 開始。

Groovy 提供各種特性使構建 DSL 更為容易,在 DSL 中支持數量是一個常見的需求。人們總是需要很多數量:7 英寸、4 英裡、13 天等。Groovy 允許您通過開放類 直接添加對數量的支持。開源類允許您重新打開現存類並通過在類中添加、刪除或修改方法對其進行修改 — 一個強大但危險的機制。幸運的是,這有安全的方法來實現這一任務。Groovy 支持兩種不同的開放類語法:categories 和 ExpandoMetaClass。

通過 categories 開放類

categories 的概念是從 Smalltalk 和 Objective-C 語言中借用的。一個 categories 可以使用 use 塊指令,圍繞代碼調用創建一個包裝器,含有一個或多個開放類。

通過一個示例更好的理解 categories 概念。清單 1 演示了我已經添加到 String 中的新方法 camelize() 的測試,該方法可以將帶下劃線的字符串轉換成駝峰式大小寫:

清單 1. 測試演示 camelize()方法

class TestStringCategory extends GroovyTestCase {
   def expected = ["event_map" : "eventMap",
       "name" : "name", "test_date" : "testDate",
       "test_string_with_lots_of_breaks" : "testStringWithLotsOfBreaks",
       "String_that_has_init_cap" : "stringThatHasInitCap" ]

   void test_Camelize() {
     use (StringCategory) {
       expected.each { key, value ->
         assertEquals value, key.camelize()
       }
     }
   }
}

在 清單 1 中,我使用原始的和轉換後的案例創建了一個 expected 散列值,然後根據映射的迭代包裝 StringCategory,希望將每個關鍵詞駝峰化(camelized)。注意在 use 塊中,您不需要特別做什麼就可以調用類中的新方法。

StringCategory 的代碼在清單 2 中顯示:

清單 2. StringCategory 類

class StringCategory {

  static String camelize(String self) {
   def newName = self.split("_").collect() {
    it.substring(0, 1).toUpperCase() + it.substring(1, it.length())
   }.join()
   newName.substring(0, 1).toLowerCase() + newName.substring(1, newName.length())
  }
}

categories 是一個常規類,包含靜態方法。靜態方法必須要有一個參數,這是您將要增加的類型。在 清單 2 中,我聲明了一個單獨的靜態方法,接收 String 參數(通常稱為 self,但是您可以隨意為其命名),代表我向其中添加方法的類。方法體包含 Groovy 代碼,通過下劃線將字符串分成帶分隔符的幾塊(這就是 split("_") 方法所做的),然後將字符串收集到一起,在合適的地方使用大寫字母將它們拼接起來。最後一行確保返回的第一個字符是小寫的。

當您使用 StringCategory 時,您必須在 use 塊中訪問它。在 use 塊的圓括號中有多個 categories 類,之間用逗號隔開,這是合法的。

這是在 DSL 中使用開放類表示數量的另一個實例,考慮清單 3 中的代碼,實現了一個預約日歷:

清單 3. 一個簡單的日歷 DSL

def calendar = new AppointmentCalendar()

use (IntegerWithTimeSupport) {
   calendar.add new Appointment("Dentist").from(4.pm)
   calendar.add new Appointment("Conference call")
          .from(5.pm)
          .to(6.pm)
          .at("555-123-4321")
}
calendar.print()

清單 3 實現了和 “連貫接口” 中 Java 實例一樣的功能,但是增強了語法,其中包括了在 Java 代碼中所不能實現的。例如,請注意 Groovy 允許在有些地方刪除括號(像圍繞 add() 方法的參數這種情況)。我也可以調用像 5.pm 這樣對開發人員來說稀奇古怪的命令。這是一個打開 Integer 類(在 Groovy 中所有的數字可以自動使用 type-wrapper 類,即使 5 是一個真正的 Integer )和添加一個 pm 屬性的實例。實現該開放類的類在清單 4 中顯示:

清單 4. IntegerWithTimeSupport 類定義

class IntegerWithTimeSupport {
   static Calendar getFromToday(Integer self) {
     def target = Calendar.instance
     target.roll(Calendar.DAY_OF_MONTH, self)
     return target
   }

   static Integer getAm(Integer self) {
     self == 12 ? 0 : self 
   }

   static Integer getPm(Integer self) {
     self == 12 ? 12 : self + 12 
   }
}

這個 categories 類包括三個 Integer 新方法:getFromToday()、getAm() 和 getPm()。注意,這些事實上是新屬性,而不是方法,在這我之所以說是新屬性,是和 Groovy 處理方法調用的方式有關。當您調用一個沒有參數的 Groovy 方法時,您必須使用一對空括號調用它,這使得 Groovy 可以分清一個屬性訪問和一個方法調用之間的區別。如果我將其擴展作為方法,我的 DSL 將需要調用 am 和 pm 擴展作為 5.pm(),這會影響 DSL 的閱讀性。我使用 DSL 一個最主要的原因是增強閱讀性,因此我想要丟棄額外的雜亂語法。在 Groovy 中您也可以通過創建擴展作為屬性進行這一操作,聲明屬性的語法和 Java 語言中的 get/set 方法對是一樣的 — 但是您可以在沒有參數的情況下調用它們。

在這個 DSL 中,測量單位是小時,這意味著我需要為 3.pm 返回 15。在構建以數量為特性的 DSL 時,您需要確定您的單位,並將它們添加到 DSL(可選)使其更可讀。記住我將使用 DSL 來捕獲一個領域慣用模式,這意味這非程序員也可以閱讀。

現在,您已經看到了在日歷 DSL 中如何實現時間,Appointment 類在清單 5 中顯示,簡單易懂:

清單 5. Appointment 類

class Appointment {
  def name;
  def location;
  def date;
  def startTime;
  def endTime;

  Appointment(apptName) {
   name = apptName
   date = Calendar.instance
  }

  def at(loc) {
   location = loc 
   this
  }

  def formatTime(time) {
   time > 12 ? "${time - 12} PM" : "${time} AM"
  }

  def getStartTime() {
   formatTime(startTime)
  }

  def getEndTime() {
   formatTime(endTime)
  }

  def from(start_time) {
   startTime = start_time
   date.set(Calendar.HOUR_OF_DAY, start_time)
   this
  }

  def to(end_time) {
   endTime = end_time
   date.set(Calendar.HOUR_OF_DAY, end_time)
   this
  }

  def display() {
   print "Appointment: ${name}, Starts: ${formatTime(startTime)}"
   if (endTime) print ", Ends: ${formatTime(endTime)}"
   if (location) print ", Location: ${location}"
   println()
  }
}

即使您一點也不了解 Groovy,閱讀 Appointment 類也沒有一點問題。注意,在 Groovy 中方法的最後一行是它的返回值。這使 at()、from() 和 to()(this 方法返回值)方法的最後一行成為該類中的流暢接口調用。

categories 允許您以一種受控的方式改變現有的類。改變被嚴格限制在由 use() 語句定義的詞典塊中。然而,有時候您需要一個開放類的添加方法來擴展范圍,這時候 Groovy 的 ExpandoMetaClass 就派上用場了。

通過 expando 開放類

Groovy 中最初的開放類語法僅使用 categories。然而,Groovy web 框架的構造器,Grails,發現 categories 固有的作用域限制太嚴格了,這導致開發了開放類另一種語法,ExpandoMetaClass。當您使用一個 expando 時,您需要訪問類的元類(這是 Groovy 為您隨機創建的)並向其中添加屬性和方法。使用 expando 的日歷示例在清單 6 中顯示:

清單 6. 使用 expando 開放類的日歷

def calendar = new AppointmentCalendar()

calendar.add new Appointment("Dentist")
        .from(4.pm)
calendar.add new Appointment("Conference call")
        .from(5.pm)
        .to(6.pm)
        .at("555-123-4321")

calendar.print() 

清單 6 中的代碼看起來和 清單 3 幾乎一樣,只是缺少了 categories 必須的 use 塊。要實現對 Integer 的改變,您需要訪問清單 7 中的元類:

清單 7. Integer 的 Expando 定義

Integer.metaClass.getAm = { ->
  delegate == 12 ? 0 : delegate
}

Integer.metaClass.getPm = { ->
  delegate == 12 ? 12 : delegate + 12
}

Integer.metaClass.getFromToday = { ->
  def target = Calendar.instance
  target.roll(Calendar.DAY_OF_MONTH, delegate)
  target
}

和 categories 實例一樣,我需要 am 和 pm 作為參數而不是作為 Integer 的方法(以便在我調用它們時,不使用括號就可以進行訪問),因此我向元類添加一個新屬性作為 Integer.metaClass.getAm。這些代碼塊可以接收參數,但在這我不需要(因此在代碼行的開始只需要一個 -> 即可)。在代碼塊中,delegate 關鍵字指向您將要向其中添加方法的類的實例。例如,注意在 getFromToday 屬性中,我創建了一個新 Calendar 實例,然後使用 delegate 值滾動日歷天數(由 Integer 實例指定的)。當我執行 5.fromToday 時,我需要將日歷向前滾動 5 天。

在 categories 和 expando 之間選擇

既然 categories 和 expandos 給您提供相同類型的表示,您選擇哪個呢? categories 的好處是固有的詞典塊范圍限制。對語言的核心類進行根本改變(可能是破壞)是一個常見的 DSL 反模式。Categories 強制使用限制來緩解。另一方面,Expandos 本質上是全局的:一旦 expando 代碼執行,這些改變將會出現在應用程序的其余部分中。

一般來說,選擇 categories,當您修改重要的類時會有潛在的副作用,您需要限制這些修改的范圍。Categories 允許您將修改范圍限制得很窄。然而,如果您發現使用相同的 categories 包裝的代碼越來越多時,您應該使用 expandos。一些修改需要被擴展,而且迫使所有修改適應塊可能會導致代碼錯綜復雜。一般說來,如果您發現自己在一個 category 中包裝超過 3 個根本不同的代碼塊時,考慮使用 expando。

最後一點:在這測試不是可選的。許多開發人員認為他們的大量代碼可以選擇測試,但是修改現有類的任何代碼都需要綜合測試。核心類的修改能力很強,能夠生成良好的問題解決方案。但是與能力隨之而來的還有責任,表現為測試。

一個示例

到目前為止,介紹 DSL 作為一種捕獲慣用模式的方法可能有點抽象,因此我接下來將使用一個真實的示例作為結束。

easyb是一個基於 Groovy 的行為驅動開發測試工具,允許您創建將非開發人員的友好格式和代碼結合的場景來實現測試。清單 8 中是一個 easyb 場景的示例:

清單 8. easyb 場景測試一個隊列

package org.easyb.bdd.specification.queue

import org.easyb.bdd.Queue

description "This is how a Queue must work"

before "initialize the queue for each spec", {
   queue = new Queue()
}

it "should dequeue item just enqueued", {
   queue.enqueue(2)
   queue.dequeue().shouldBe(2)
}

it "should throw an exception when null is enqueued", {
   ensureThrows(RuntimeException.class) {
     queue.enqueue(null)
   }
}

it "should dequeue items in same order enqueued", {
   [1..5].each {val ->
     queue.enqueue(val)
   }
   [1..5].each {val ->
     queue.dequeue().shouldBe(val)
   }
}

清單 8 中的代碼為隊列定義了適當的行為。每個聲明塊以 it 開始,後面是一個字符串描述和一個代碼塊。it 的方法定義看起來像這樣,其中 spec 預計將描述測試,而 closure 保持代碼塊:

def it(spec, closure)

注意在 清單 8 最後一行,我驗證了來自調用 dequeue() 的值,使用下面這行代碼:

queue.dequeue().shouldBe(val)

但是 Queue 類的檢查顯示它沒有一個 shouldBe()方法。那麼它是從哪裡來的呢?

如果您查看 it() 方法的定義,您將可以看到 categories 用在何處來擴展已有類。清單 9 顯示 it() 方法的聲明:

清單 9. it() 方法的聲明

def it(spec, closure) {
   stepStack.startStep(listener, BehaviorStepType.IT, spec)
   closure.delegate = new EnsuringDelegate()
   try {
     if (beforeIt != null) {
       beforeIt()
     }
     listener.gotResult(new Result(Result.SUCCEEDED))
   use(BehaviorCategory) {
       closure()
     }
     if (afterIt != null) {
       afterIt()
     }
   } catch (Throwable ex) {
     listener.gotResult(new Result(ex))
   }
   stepStack.stopStep(listener)
}

大約一半的方法,作為參數傳遞的封閉塊將在 BehaviorCategory 類中執行,清單 10 顯示了其中的一部分:

清單 10. BehaviorCategory 類的一部分

static void shouldBe(Object self, value, String msg) {
   isEqual(self, value, msg)
}

private static void isEqual(self, value, String msg) {
   if (self.getClass() == NullObject.class) {
     if (value != null) {
       throwValidationException( 
         "expected ${value.toString()} but target object is null", msg)
     }
   } else if (value.getClass() == String.class) {
     if (!value.toString().equals(self.toString())) {
       throwValidationException( 
         "expected ${value.toString()} but was ${self.toString()}", msg)
     }
   } else {
     if (value != self) {
       throwValidationException("expected ${value} but was ${self}", msg)
     }
   }
}

BehaviorCategory 是一個 category ,其方法擴增了 Object,闡述了開放類令人難以置信的力量。向 Object 添加一個新方法,這使得向每個類(包括 Queue)添加一個 shouldBe() 方法變得十分容易。您不能使用核心 Java 代碼進行這一操作,這樣做太麻煩了,甚至沒法入手。categories 的使用加強了我之前的建議:它將對 Object 的修改范圍限制為 easyb DSL 中 use 子句。

結束語

我想讓我捕獲的慣用模式從其余的代碼中脫穎而出,DSL 提供一個令人信服的機制來實現這一目標。使用支持它們的語言編寫 DSL 非常容易,不像使用 Java 語言那樣。如果您組織的外部力量妨礙您利用非 Java 語言的優勢,不要放棄。像 Spring 框架這類語言越來越多地支持其他語言,比如 Groovy 或 Clojure。您可以使用這些語言來創建部件並讓 Spring 將其注入到您應用程序的合適位置。許多組織在使用其他語言方面太過保守,但是通過 Spring 這類框架很容易增加線路。

下一期中,我將使用幾個 JRuby 實例集中精力介紹使用 DSL 作為一種方法捕獲領域慣用模式,闡述可以將語言的表現力呈現得多深入 。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved