在本期 函數式思維 的文章中,我將繼續研究 Gang of Four (GoF) 設計模式(參閱 參考資料)的函數式替代解決方案。在本文中,我將研究最少人了解,但卻是最強大的模式之一:解釋器 (Interpreter)。
解釋器的定義是:
給定一個語言,定義其語法表示,以及一個使用該表示來解釋語言中的句子的解釋器。
換句話說,如果您正在使用的語言不適用于解決問題,那么用它來構建一個適用的語言。關于該方法的一個很好的示例出現在 Web 框架中,如 Grails 和 Ruby on Rails(參閱 參考資料),它們擴展了自己的基礎語言(分別是 Groovy 和 Ruby),使編寫 Web 應用程序變得更容易。
這種模式最少人了解,因為構建一種新的語言并不常見,需要專業的技能和慣用語法。它是最強大的 設計模式,因為它鼓勵您針對正在解決的問題擴展自己的編程語言。這在 Lisp(因此 Clojure 也同樣)世界是一個普遍的特質,但在主流語言中不太常見。
當使用禁止對語言本身進行擴展的語言(如 Java)時,開發人員往往將自己的思維塑造成該語言的語法;這是您的惟一選擇。然而,當您漸漸習慣使用允許輕松擴展的語言時,您就會開始將語言折向解決問題的方向,而不是其他折衷的方式。
Java 缺乏直觀的語言擴展機制,除非您求助于面向方面的編程。然而,下一代的 JVM 語言(Groovy、Scala 和 Clojure)(參閱 參考資料)均支持以多種方式進行擴展。通過這樣做,它們可以達到解釋器設計模式的目的。首先,我將展示如何使用這三種語言實現操作符重載,然后演示 Groovy 和 Scala 如何讓您擴展現有的類。
操作符重載 是函數式語言的一個常見特性,能夠重定義操作符(如 +
、-
或 *
)配合新的類型工作,并表現出新的行為。操作符重載的缺失是 Java 形成時期的一個有意識的決定,但現在幾乎每一個現代語言都具備這個特性,包括在 JVM 上 Java 的天然接班人。
Groovy 嘗試更新 Java 的語法,使其跟上潮流,同時保留其自然語義。因此,Groovy 通過將操作符自動映射到方法名稱實現操作符重載。例如,如果您想重載 Integer
的 +
操作符,那么您要重寫 Integer
類的 plus()
方法。完整的映射列表已在線提供(參閱 參考資料);表 1 顯示了列表的一部分:
表 1. Groovy 的操作符/方法映射列表的一部分
操作符 | 方法 |
---|---|
x + y | x.plus(y) |
x * y | x.multiply(y) |
x / y | x.div(y) |
x ** y | x.power(y) |
作為一個操作符重載的示例,我將在 Groovy 和 Scala 中都創建一個 ComplexNumber
類。復數 是一個數學概念,由一個實數 和虛數 部分組成,一般寫法是,例如 3 + 4i
。復數在許多科學領域中都很常用,包括工程學、物理學、電磁學和混沌理論。開發人員在編寫這些領域的應用程序時,大大受益于能夠創建反映其問題域的操作符。(有關復數的更多信息,請參閱 參考資料。)
清單 1 中顯示了一個 Groovy ComplexNumber
類:
清單 1. Groovy 中的
ComplexNumber
package complexnums class ComplexNumber { def real, imaginary public ComplexNumber(real, imaginary) { this.real = real this.imaginary = imaginary } def plus(rhs) { new ComplexNumber(this.real + rhs.real, this.imaginary + rhs.imaginary) } def multiply(rhs) { new ComplexNumber( real * rhs.real - imaginary * rhs.imaginary, real * rhs.imaginary + imaginary * rhs.real) } String toString() { real.toString() + ((imaginary < 0 ? "" : "+") + imaginary + "i").toString() } } |
在 清單 1 中,我創建一個類,保存實數和虛數部分,并且我創建重載的 plus()
和 multiply()
操作符。兩個復數的相加是非常直觀的:plus()
操作符將兩個數各自的實數和虛數分別進行相加,并產生結果。兩個復數的相乘需要以下公式:
(x + yi)(u + vi) = (xu - yv) + (xv + yu)i |
在 清單 1 中的 multiply()
操作符復制該公式。它將兩個數字的實數部分相乘,然后減去虛數部分相乘的積,再加上實數和虛數分別彼此相乘的積。
清單 2 測試復數運算符:
清單 2. 測試復數運算符
package complexnums import org.junit.Test import static org.junit.Assert.assertTrue import org.junit.Before class ComplexNumberTest { def x, y @Before void setup() { x = new ComplexNumber(3, 2) y = new ComplexNumber(1, 4) } @Test void plus_test() { def z = x + y; assertTrue 3 + 1 == z.real assertTrue 2 + 4 == z.imaginary } @Test void multiply_test() { def z = x * y assertTrue(-5 == z.real) assertTrue 14 == z.imaginary } } |
在 清單 2 中,plus_test()
和 multiply_test()
方法對重載操作符的使用(兩者都以該領域專家使用的相同符號代表)與類似的內置類型用法沒什么區別。
Scala 通過放棄操作符和方法之間的區別來實現操作符重載:操作符僅僅是具有特殊名稱的方法。因此,要使用 Scala 重寫乘法運算,您要重寫 *
方法。在清單 3 中,我用 Scala 創建復數。
清單 3. Scala 中的復數
class ComplexNumber(val real:Int, val imaginary:Int) { def +(operand:ComplexNumber):ComplexNumber = { new ComplexNumber(real + operand.real, imaginary + operand.imaginary) } def *(operand:ComplexNumber):ComplexNumber = { new ComplexNumber(real * operand.real - imaginary * operand.imaginary, real * operand.imaginary + imaginary * operand.real) } override def toString() = { real + (if (imaginary < 0) "" else "+") + imaginary + "i" } } |
清單 3 中的類包括熟悉的 real
和 imaginary
成員,以及 +
和 *
操作符/方法。如清單 4 所示,我可以自然地使用 ComplexNumber
:
清單 4. 在 Scala 中使用復數
val c1 = new ComplexNumber(3, 2) val c2 = new ComplexNumber(1, 4) val c3 = c1 + c2 assert(c3.real == 4) assert(c3.imaginary == 6) val res = c1 + c2 * c3 printf("(%s) + (%s) * (%s) = %s\n", c1, c2, c3, res) assert(res.real == -17) assert(res.imaginary == 24) |
通過統一操作符和方法,Scala 使操作符重載變成一件小事。Clojure 使用相同的機制來重載操作符。例如,以下 Clojure 代碼定義了一個重載的 **
操作符:
(defn ** [x y] (Math/pow x y)) |
類似于操作符重載,下一代的 JVM 語言允許您擴展類(包括核心 Java 類),擴展的方式在 Java 語言本身是不可能實現的。這些設施通常用于構建領域特定的語言 (DSL)。雖然 GOF 從來沒有考慮過 DSL(因為它們與當時流行的語言沒有共同點),DSL 卻體現了解釋器設計模式的初衷。
通過將計量單位和其他修飾符添加給 Integer
等核心類,您可以(就像添加操作符一樣)更緊密地對現實問題進行建模。Groovy 和 Scala 都支持這樣做,但它們使用不同的機制。
Groovy 包括兩種對現有類添加方法的機制:ExpandoMetaClass
和類別。(在 函數式思維:函數設計模式,第 2 部分 中,我在適配器模式的上下文中詳細介紹過 ExpandoMetaClass
。)
比方說,您的公司由于離奇的遺留原因,需要以浪(furlongs,英國的計量單位)/每兩周而不是以英里/每小時 (MPH) 的方法來表達速度,開發人員發現自己經常要執行這種轉換。使用 Groovy 的 ExpandoMetaClass
,您可以添加一個 FF
屬性給處理轉換的 Integer
,如清單 5 所示:
清單 5. 使用
ExpandoMetaClass
添加一個浪/兩周的計量單位給 Integer
static { Integer.metaClass.getFF { -> delegate * 2688 } } @Test void test_conversion_with_expando() { assertTrue 1.FF == 2688 } |
ExpandoMetaClass
的替代方法是,創建一個類別 包裝器類,這是從 Objective-C 借來的概念。在清單 6 中,我添加了一個(小寫) ff
屬性給 Integer
:
清單 6. 通過一個類別類添加計量單位
class FFCategory { static Integer getFf(Integer self) { self * 2688 } } @Test void test_conversion_with_category() { use(FFCategory) { assertTrue 1.ff == 2688 } } |
一個類別類是一個帶有一組靜態方法集合的普通類。每個方法接受至少一個參數;第一個參數是這種方法增強的類型。例如,在 清單 6 中, FFCategory
類擁有一個 getFf()
方法,它接受一個 Integer
參數。當這個類別類與 use
關鍵字一起使用時,代碼塊內所有相應類型都被增強。在單元測試中,我可以在代碼塊內引用 ff
屬性(記住,Groovy 自動將 get
方法轉換為屬性引用),如在 清單 6 的底部所示。
有兩種機制可供選擇,讓您可以更準確地控制增強的范圍。例如,如果整個系統使用 MPH 作為速度的默認單位,但也需要頻繁轉換為浪/每兩周,那么使用 ExpandoMetaClass
進行全局修改將是適當的。
您可能對重新開放核心 JVM 類的有效性持懷疑態度,擔心會產生廣泛深遠的影響。類別類讓您限制潛在危險性增強的范圍。以下是一個來自真實世界的開源項目示例,它極好地利用了這一機制。
easyb 項目(參閱 參考資料)讓您可以編寫測試,以驗證正接受測試的類的各個方面。請研究清單 7 所示的 easyb 測試代碼片段:
清單 7. easyb 測試一個
queue
類it "should dequeue items in same order enqueued", { [1..5].each {val -> queue.enqueue(val) } [1..5].each {val -> queue.dequeue().shouldBe(val) } } |
queue
類不包括 shouldBe()
方法,這是我在測試的驗證階段所調用的方法。easyb 框架已為我添加了該方法;清單 8 中所顯示的在 easyb 源代碼中的 it()
方法定義,演示了該過程:
清單 8. easyb 的
it()
方法定義 def it(spec, closure) { stepStack.startStep(BehaviorStepType.IT, spec) closure.delegate = new EnsuringDelegate() try { if (beforeIt != null) { beforeIt() } listener.gotResult(new Result(Result.SUCCEEDED)) use(categories) { closure() } if (afterIt != null) { afterIt() } } catch (Throwable ex) { listener.gotResult(new Result(ex)) } finally { stepStack.stopStep() } } class BehaviorCategory { // ... static void shouldBe(Object self, value) { shouldBe(self, value, null) } //... } |
在 清單 8中,it()
方法接受了一個 spec (描述測試的一個字符串)和一個代表測試的主體的閉包塊。在方法的中間,閉包會在 BehaviorCategory
塊內執行,該塊出現在清單的底部。BehaviorCategory
增強 Object
,允許 Java 世界中的任何 實例驗證其值。
通過允許選擇性增強駐留在層次結構頂層的 Object
,Groovy 的開放類機制可以輕松地實現為任何實例驗證結果,但它限制了對 use
塊主體的修改。
Scala 使用隱式轉換 來模擬現有類的增強。隱式轉換不會對類添加方法,但允許語言自動將一個對象轉換成擁有所需方法的相應類型。例如,我不能將 isBlank()
方法添加到 String
類中,但我可以創建一個隱式轉換,將 String
自動轉換為擁有這種方法的類。
作為一個示例,我想將 append()
方法添加到 Array
,這讓我可以輕松地將 Person
實例添加到適當類型的數組,如清單 9 所示:
清單 9.將一個方法添加到
Array
中,以增加人員case class Person (firstName: String, lastName: String) {} class PersonWrapper(a: Array[Person]) { def append(other: Person) = { a ++ Array(other) } def +(other: Person) = { a ++ Array(other) } } implicit def listWrapper(a: Array[Person]) = new PersonWrapper(a) |
在 清單 9中,我創建一個簡單的 Person
類,它帶有若干個屬性。為了使 Array[Person]
(在 Scala 中,一般使用 [ ]
而不是 < >
作為分隔符)Person
可知,我創建一個 PersonWrapper
類,它包括所需的 append()
方法。在清單的底部,我創建一個隱式轉換,當我在數組上調用 append()
方法時,隱式轉換會自動將一個 Array[Person]
轉換為 PersonWrapper
。清單 10 測試該轉換:
清單 10. 測試對現有類的自然擴展
val p1 = new Person("John", "Doe") var people = Array[Person]() people = people.append(p1) |
在 清單 9中,我也為 PersonWrapper
類添加了一個 +
方法。清單 11 顯示了我如何使用操作符的這個漂亮直觀的版本:
清單 11. 修改語言以增強可讀性
people = people + new Person("Fred", "Smith") for (p <- people) printf("%s, %s\n", p.lastName, p.firstName) |
Scala 實際上并未對原始的類添加一個方法,但它通過自動轉換成一個合適的類型,提供了這樣做的外觀。使用 Groovy 等語言進行元編程所需要的相同工作在 Scala 中也需要,以避免過多使用隱式轉換而產生由相互關聯的類所組成的令人費解的網。但是,在正確使用時,隱式轉換可以幫助您編寫表達非常清晰的代碼。
來自 GoF 的原始解釋器設計模式建議創建一個新語言,但其基礎語言并不支持我們今天所掌握的良好擴展機制。下一代 Java 語言都通過使用多種技術來支持語言級別的可擴展性。在本期文章中,我演示了操作符重載如何在 Groovy、Scala 和 Clojure 中工作,并研究了在 Groovy 和 Scala 中的類擴展。
在下期文章中,我將展示 Scala 風格的模式匹配和泛型的組合如何取代一些傳統的設計模式。該討論的中心是一個在函數式錯誤處理中也起著作用的概念,這一概念將是我們下期文章的主題。
學習
- The Productive Programmer(Neal Ford,O'Reilly Media,2008 年):Neal Ford 的新書討論了幫助您提高編碼效率的工具和實踐。
- Design Patterns: Elements of Reusable Object-Oriented Software(Erich Gamma 等人,Addison-Wesley,1994 年):關于 Gang of Four 在設計模式方面的經典之作。
- Complex number:復數是數學抽象,在許多科學領域中發揮作用。
- Scala:Scala 是一種現代函數編程語言,適用于 JVM。
- Clojure:Clojure 是一種現代函數式 Lisp,適用于 JVM。
- Groovy:Groovy 是一種現代動態 JVM 語言,具有多種函數方面。
- Operator overloading in Groovy:此頁顯示 Groovy 中支持的操作符以及其映射方法的完整列表。
- "實戰 Groovy: 使用閉包、ExpandoMetaClass 和類別進行元編程"(Scott Davis,developerWorks,2009 年 6 月):了解有關在 Groovy 中元編程的更多信息。
- easyb:easyb 是一個使用 Groovy 開發的行為驅動開源開發工具,適用于 Groovy 和 Java 項目。
- "Drive development with easyb"(Andrew Glover,developerWorks,2008 年 11 月):了解 easyb 如何幫助開發人員及利益相關者進行協作。
- Grails:Grails 是使用 Java 和 Groovy 編寫的一種開源 Web 框架。
- Ruby on Rails:Rails 是使用 Ruby 編寫的一種開源 Web 框架,運行于 JRuby 上。
- 瀏覽 技術書店,閱讀有關這些主題和其他技術主題的圖書。
- developerWorks 中國網站 Java 技術專區:這里有數百篇關于 Java 編程各個方面的文章。
獲得產品和技術
- 下載 IBM 產品評估試用版軟件 或 IBM SOA 人員沙箱,并開始使用來自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的應用程序開發工具和中間件產品。
討論
- 查看 developerWorks 博客,并加入 developerWorks 中文社區。

Neal Ford 是一家全球性 IT 咨詢公司 ThoughtWorks 的軟件架構師和 Meme Wrangler。他的工作還包括設計和開發應用程序、教材、雜志文章、課件和視頻/DVD 演示,而且他是各種技術書籍的作者或編輯,包括最近的新書 The Productive Programmer 。他主要的工作重心是設計和構建大型企業應用程序。他還是全球開發人員會議上的國際知名演說家。請訪問他的 Web 站點。