簡介:發現和積累慣用模式的能力對於緊急設計至關重要。對於設計而言同樣十分重要的是代碼的表 達性。在本系列文章的第 2 部分中,Neal Ford 將繼續討論表達性和模式的交集,通過慣用模式和正式 設計模式闡釋這些概念。他用動態語言為 JVM 重構了一些經典的四人組(Gang of Four)模式,以說明 表達性更好的語言如何使您看到被透明度不佳的語言遮擋的設計元素。
本文是本系列文章的第 2 部分,旨在演示計算機語言的表達性(允許您專注於本質,而不是形式)對 於緊急設計的重要作用。意圖(intent)與結果(result)之間的分歧對於許多年代久遠的語言(包括 Java™ 語言)來說都是一個通病,從而為問題解決工作添加了不必要的形式。表達性更好的語言可 以幫助開發人員更加輕松地發現慣用模式,因為代碼中包含的無用信息更少。表達性是 Groovy 和 Scala 等現代語言的特征;年代久遠但表達性較好的語言包括 Ruby(其中,JRuby 是一種 JVM 變體);其他表 達性較好的語言還包括經過翻新的 Clojure,以及基於 JVM 的現代 Lisp。在本文中,我將繼續 第 1 部 分 中的演示 — 使用表達性更好的語言實現設計模式 一書中的傳統四人組模式。
修飾符模式
四人組的書籍將修飾符模式定義為:
將額外的責任動態賦予某個對象。修 飾符提供了另外一種靈活的用於擴展功能的繼承方法。
如果您曾經使用過 java.io.* 包,則應該 對修飾符模式有所了解。顯然,I/O 庫的設計者們閱讀了四人組書籍的修飾符部分,並領悟了其核心意義 !首先,我將演示修飾符模式在 Groovy 中的傳統實現,然後再在後續示例中提高它的動態性。
傳統的修飾符
清單 1 顯示了一個 Logger 類,以及與該類相關的一些修飾符 (TimeStampingLogger 和 UpperLogger),所有代碼均在 Groovy 中實現:
清單 1. Logger 和 兩個修飾符
class Logger {
def log(String message) {
println message
}
}
class TimeStampingLogger extends Logger {
private Logger logger
TimeStampingLogger(logger) {
this.logger = logger
}
def log(String message) {
def now = Calendar.instance
logger.log("$now.time: $message")
}
}
class UpperLogger extends Logger {
private Logger logger
UpperLogger(logger) {
this.logger = logger
}
def log(String message) {
logger.log(message.toUpperCase())
}
}
Logger 是一個簡單的日志程序,用於將日志消息寫入控制台。TimeStampingLogger 通過修飾添加了 一個時間戳,而 UpperLogger 用於將日志消息更改為大寫。要使用這些修飾符,需要使用適當的修飾符 封裝一個 Logger 實例,如清單 2 所示:
清單 2. 使用修飾符封裝日志程序
def logger = new UpperLogger(
new TimeStampingLogger(
new Logger()))
logger.log("Groovy Rocks")
清單 2 的輸出顯示了一個大寫的、帶時間戳的日志消息:
Tue May 22 07:13:50 EST 2007: GROOVY ROCKS
目前為止,這個修飾符唯一不尋常的地方就是它所使用的 Groovy 實現。但是,我在創建修飾符時可 以不用添加額外的基於類的方法結構。
准備修飾
四人組書籍中的傳統設計模式假定每個問題的解決方案都構建構建更多的類。但是,基於 JVM 的現代 語言提供了一些額外的便利性,比如說開放類,它允許您重新打開已有類並向它們添加新的方法,而不需 要子類化過程。當您需要更改基礎架構某部分(例如,集合 API)所使用的某個類的行為,而該行為又需 要某個特定的類時,這種方式極為方便。您可以修改已有類,將它作為參數傳遞,並利用 API,而不需要 基礎 API 聲明一個抽象類或接口。開放類還允許您執行 “就地” 修改,而不需要子類化過程。
但是,修改整個類定義聽起來有點令人擔心:您可能不希望對整個類執行全面修改。幸運的是, Groovy 和 Ruby 都允許您向單個類實例 添加新的方法。換句話說,您可以向某個 Logger 實例添加一個 新方法,而不會影響它的所有其他實例。清單 3 顯示如何使用 ExpandoMetaClass 在 Groovy 中重寫某 個 Logger 實例的 log() 方法:
清單 3. 重寫某個 Logger 實例的 log() 方法
def logger = new Logger()
logger.metaClass.log = { String m ->
println m.toUpperCase()
}
logger.log "this log message brought to you in upper case"
理解了此機制的工作原理之後,閱讀此代碼要比閱讀使用額外類的相應代碼更加輕松。所有相關的修 飾代碼都出現在一個位置,而不是分散於若干個文件中(因為在 Java 語言中,每個公有類都必須位於它 自己的文件中)。
Ruby 也提供了相同的功能,即所謂的 singleton method(這是一個令人疑惑的名稱,因為 singleton 代表著負載過重)或者 eigenclass。在 JRuby 中實現的代碼如清單 4 所示:
清單 4. 使用 Ruby 的 eigenclass 執行就位修飾
class Logger
def log(msg)
puts msg
end
end
l = Logger.new
def l.log m
puts m.upcase
end
l.log "this log message brought to you in upper case"
Ruby 版本並未使用額外的工具,比如說 ExpandoMeta Class。在 Ruby 中,您可以為某個特定的實例 定義一個內聯方法,其方法是將變量名稱放在方法聲明的最前面。Ruby 具備極佳的語法靈活性,因此對 可以在何時及何處定義方法並沒有太多規則限制。
這工具還適用於內置 Java 類。舉例來說,應該使用 first() 和 last() 方法來定義 ArrayList 類 ,不過並未采用這種方式。但是,在 Groovy 中添加這些方法是相當簡單的,如清單 5 所示:
清單 5. 在 Groovy 中為 ArrayList 添加 first() 和 last() 方法
ArrayList.metaClass.getFirst {
delegate.size > 0 ? get(0) : null
}
ArrayList.metaClass.getLast {
delegate.size > 0 ? get(delegate.size - 1) : null
}
ArrayList l = new ArrayList()
l << 1 << 2 << 3
println l.first
println l.last
ArrayList emptyList = new ArrayList()
println emptyList.first
println emptyList.last
使用 ExpandoMetaClass,您可以為類定義一些新的屬性(使用熟悉的 Java get/set 命名模式)。為 類定義了新的屬性之後,可以將它們像普通屬性一樣調用。
您可以像在 JRuby 中一樣使用已有的 JDK 類實現相同的目的,如清單 6 所示:
清單 6. 使用 JRuby 為 ArrayList 添加方法
require 'java'
include_class 'java.util.ArrayList'
class ArrayList
def first
size != 0 ? get(0) : nil
end
def last
size != 0 ? get(size - 1) : nil
end
end
list = ArrayList.new
l << 1 << 2 << 3
puts list.first
puts list.last
empty_list = ArrayList.new
puts empty_list.first
puts empty_list.last
不要錯誤地認為每個問題的解決方案都需要更多的類。元編程通常能提供更加簡潔的解決方案。
帶調用鉤子的修飾符
有時,您需要修飾能覆蓋更多的類。舉例來說,您可能希望使用事務控件修飾所有的數據庫操作。為 每個操作創建一個簡單的傳統修改器過於麻煩,並且會向代碼添加大量語法,從而造成難以確定目標工作 單元。
參見清單 7 中在 Groovy 中實現的修飾符:
清單 7. Groovy 中的 GenericLowerDecorator
class GenericLowerDecorator {
private delegate
GenericLowerDecorator(delegate) {
this.delegate = delegate
}
def invokeMethod(String name, args) {
def newargs = args.collect{ arg ->
if (arg instanceof String) return arg.toLowerCase()
else return arg
}
delegate.invokeMethod(name, newargs)
}
}
GenericLowerDecorator 類充當一個通用修飾符,用於強制所有基於字符串的參數使用小寫形式。它 通過使用 hook 方法來實現此目的。調用這個修飾符時,需要將它封裝在任意實例內部。invokeMethod() 方法將截取調用此類的所有方法,這樣您便可以執行任何所需的操作。在本例中,我截取了各個方法調用 ,並遍歷了所有的方法參數。如果有任何參數屬於 String 類型,則將該參數的小寫版本添加一個新的參 數列表中,並保留其他參數不變。在鉤子方法的結束部分,我使用新參數列表對修飾對象調用原始方法。 此修飾符會將所有字符串參數轉換為小寫形式,而與方法或它的參數無關。清單 8 顯示了一個應用示例 ,它對 清單 1 中的日志程序進行了封裝:
清單 8. 使用 GenericLowerDecorator 操作 Logger
logger = new GenericLowerDecorator(
new TimeStampingLogger(
new Logger()))
logger.log('IMPORTANT Message')
使用此修飾符調用的任何方法都將只使用小寫字符串:
Tue May 22 07:27:18 EST 2007: important message
注意,時間戳並未使用小寫形式,而 String 參數變為了小寫形式。這可以在 Java 語言中實現但非 常困難。事實上,使用視點(比如說通過 AspectJ)是在 Java 語言中實現此效果的唯一方法。要獲取這 種類型的修飾符,您必須切換為另一個帶有獨立編譯器的語言,並為您的 Java 代碼設置後期處理。雖然 說並不是不可能,但其流程可能會是難以想象的麻煩。
適配器模式
四人組的書將適配器模式定義為:
將某個類的接口轉換為接口客戶所需的類型。適配器允許各類共同工作(由於接口不兼容,因為無法 通過其他方式實現此目的)。
如果您使用過 Swing 中的事件處理程序,則應該對適配器模式有一定的了解。它用於圍繞包含多個方 法的事件處理接口創建適配器類,這樣您就不需要創建自己的類,實現接口,以及導入大量空方法。 Swing 適配器允許您子類化適配器,並且可以僅重寫處理事件所需的方法。
Groovy 中的適配
從根本上說,適配器模式嘗試解答的問題是:“我能否讓這個方形木條適合這個圓孔?”這正是本文 將要解決的問題。我將使用兩個不同的實現,分別強調了表達性對於各語言的重要性。第一個實現將使用 Groovy;清單 9 給出了相關的三個類和一個接口:
清單 9. 方形木條和圓孔
interface RoundThing {
def getRadius()
}
class SquarePeg {
def width
}
class RoundPeg {
def radius
}
class RoundHole {
def radius
def pegFits(peg) {
peg.radius <= radius
}
String toString() { "RoundHole with radius $radius" }
}
傳統的適配器實現將創建一個 SquarePegAdaptor 類,它封裝了方形木條並實現了 RoundHole 的 pegFits() 方法所所需的 getRadius() 方法。但是,Groovy 允許我繞過額外類的結構,以內聯的方式直 接定義適配,如清單 10 所示:
清單 10. 測試內聯適配器
@Test void pegs_and_holes() {
def adapter = { p ->
[getRadius:{Math.sqrt(
((p.width/2) ** 2)*2)}] as RoundThing
}
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = new SquarePeg(width:w)
if (w < 6)
assertTrue hole.pegFits(adapter(peg))
else
assertFalse hole.pegFits(adapter(peg))
}
}
適配器定義看上去有些奇怪,但它封裝了大量功能。我將 adaptor 定義為一個代碼塊(使用 Groovy 中的 { 界定)。在代碼塊內部,我創建了一個散列,其鍵是某個屬性的名稱(getRadius()),而值是實 現了適配器所需功能的代碼塊。Groovy 中的 as 運算符完成的神奇的工作。當我對某代碼塊執行 as 運 算符時,Groovy 將創建一個實現了 RoundThing 接口的新類;對該類的方法調用將在散列中執行查找操 作,將方法名與鍵值匹配,並執行相應的代碼塊。最終的結果是一個高度輕量級的適配器類,它實現了 RoundThing 接口所需的功能。
雖然最終的類級實現與傳統方法相同,但其代碼(如果您了解 Groovy)更易於閱讀和理解。僅在這種 情況下,Groovy 允許您圍繞接口創建輕量級的包裝器類。
JRuby 中的適配器模式
如果您完全不願意為適配器創建額外的類,那麼應該怎麼辦呢?Groovy 和 Ruby 都支持開放開放類, 因此您可以直接在相關類中添加所需的方法。Ruby 中的方形木條和圓孔實現(通過 JRuby)如清單 11 所示:
清單 11. Ruby 中的開放類適配器
class SquarePeg
attr_reader :width
def initialize(width)
@width = width
end
end
class SquarePeg
def radius
Math.sqrt(((@width/2) ** 2) * 2 )
end
end
class RoundPeg
attr_reader :radius
def initialize(radius)
@radius = radius
end
def width
@radius * @radius
end
end
class RoundHole
attr_reader :radius
def initialize(r)
@radius = r
end
def peg_fits?( peg )
peg.radius <= radius
end
end
清單 11 中的第二個 SquarePeg 類定義並沒有錯:Ruby 的開放類語法看上去類似於普通的定定義。 在使用某個類名時,Ruby 會檢查是否已經從類路徑中加載了相同名稱的類,如果是,則會在第二次操作 時重新打開這個類。當然,在本例中,我可以將 radius() 方法直接添加到類中,但我假定原始的 SquarePeg 類在此代碼之前定義。清單 12 顯示了開放類適配器的單元測試:
清單 12. 測試開放類適配器
def test_open_class_pegs
hole = RoundHole.new( 4.0 )
4.upto(7) do |i|
peg = SquarePeg.new(i.to_f)
if (i < 6)
assert hole.peg_fits?(peg)
else
assert ! hole.peg_fits?(peg)
end
end
end
在本例中,我可以直接對 SquarePeg 類調用 radius 方法,因為它已經包含一個 radius 方法。通過 開放類添加一個方法可以完全避免對單獨適配器類的需要,無論是通過手寫還是自動生成。但是,此代碼 存在一個潛在的問題:如果 SquarePeg 類已經包含一個與圓孔沒有任何關系的 radius 方法,那又該怎 麼辦呢?使用開放類會重寫這個原始類,從而導致意外行為。
這正是表達性語言的強大之處。考慮如清單 13 所示的 Ruby 代碼:
清單 13. 接口切換
class SquarePeg
include InterfaceSwitching
def radius
@width
end
def_interface :square, :radius
def radius
Math.sqrt(((@width/2) ** 2) * 2)
end
def_interface :holes, :radius
def initialize(width)
set_interface :square
@width = width
end
end
此代碼基本上無法使用 Java 語言或 Groovy 實現。注意,我使用 radius 這個名稱定義了兩個方法 。在 Groovy 中,編譯器不會編譯此代碼。但是,Ruby(以及 JRuby)是一種解釋語言,它允許您在解釋 過程中 執行代碼。某些 Ruby 愛好者將 Ruby 中的結構(constructs)稱為 “一等市民”,表示該語言 的所有部分都是隨時可用的。此處的魔力在於(類似於關鍵字)def_interface 方法調用。這是對在解釋 時執行的 Class 類定義的一個元編程方法。這代碼允許您為某個方法定義一個特定的接口,並設定該方 法只能存在於特定的作用域內。此作用域是由 with_interface 方法調用定義的,如清單 14 所示:
清單 14. 測試接口切換
def test_pegs_switching
hole = RoundHole.new( 4.0 )
4.upto(7) do |i|
peg = SquarePeg.new(i)
peg.with_interface(:holes) do
if (i < 6)
assert hole.peg_fits?(peg)
else
assert ! hole.peg_fits?(peg)
end
end
end
end
在 with_interface 代碼塊的作用域中,可以調用使用該接口名定義的 radius 方法。清單 15 中的 代碼實現了此功能,其結構不但緊湊而且相當簡潔。其作用是提供上下文參考;其大多數內容都是比較高 級的 Ruby 元編程,因此本文並不會詳細討論它。
清單 15. 接口切換魔力
class Class
def def_interface(interface, *syms)
@__interface__ = {}
a = (@__interface__[interface] = [])
syms.each do |s|
a << s unless a.include? s
alias_method "__#{s}_#{interface}__".intern, s
remove_method s
end
end
end
module InterfaceSwitching
def set_interface(interface)
unless self.class.instance_eval{ @__interface__[interface] }
raise "Interface for #{self.inspect} not understood."
end
i_hash = self.class.instance_eval "@__interface__[interface]"
i_hash.each do |meth|
class << self; self end.class_eval <<-EOF
def #{meth}(*args, &block)
send(:__#{meth}_#{interface}__, *args, &block)
end
EOF
end
@__interface__ = interface
end
def with_interface(interface)
oldinterface = @__interface__
set_interface(interface)
begin
yield self
ensure
set_interface(oldinterface)
end
end
end
清單 15 中比較有趣的地方是開放類 Class 定義的結束部分:為指定方法賦予了另一個名稱(基於接 口),然後通過代碼將它從代碼中刪除。更加有趣的代碼出現在 InterfaceSwitching 中: set_interface 方法為在 with_interface 方法中創建的代碼塊的作用域重新定義了原始(重命名的)方 法。最後的 ensure 代碼塊是 Ruby 版本的 finally 代碼塊。
此練習的目的並不是深入探討 Ruby 中的充滿魔力的元編程,而是演示表達性極佳的語言能實現哪些 功能。解釋語言始終比編譯語言具有更大的優勢,因為它們可以執行編譯語言無法執行的代碼。事實上, Groovy 引入了一種編譯時元編程機制,即 AST Transformations。
結束語
本文的所有這些論述證明了什麼呢?在各種語言中,表達性就相當於其能力。本文中介紹的許多技巧 都是 Java 語言所不支持的,甚至使用 Javassist 等工具提供的字節碼生成功能也無法從技術上實現它 們。但是,使用這些機制來解決問題可以說是極其麻煩的。此態度也影響了慣用模式。即便您可以看到特 定於應用程序的模式,但如果獲得收益的方法過於困難,則會讓您的項目背上數不清的技術債務。表達性 對計算機語言的重要性是不言而喻的!