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

Neal Ford 是一家全球性 IT 咨詢公司 ThoughtWorks 的軟件架構(gòu)師和 Meme Wrangler。他的工作還包括設(shè)計(jì)和開(kāi)發(fā)應(yīng)用程序、教材、雜志文章、課件和視頻/DVD 演示,而且他是各種技術(shù)書(shū)籍的作者或編輯,包括最近的新書(shū) The Productive Programmer 。他主要的工作重心是設(shè)計(jì)和構(gòu)建大型企業(yè)應(yīng)用程序。他還是全球開(kāi)發(fā)人員會(huì)議上的國(guó)際知名演說(shuō)家。請(qǐng)?jiān)L問(wèn)他的 Web 站點(diǎn)。