意圖與實現的鴻溝導致樣板代碼掩蓋設計意圖。表達性強的語言(Groovy、Ruby、Scala、Clojure)讓開發者聚焦本質,更易發現慣用模式。本文用 JVM 動態語言重構 GoF 修飾器與適配器模式,展示表達性如何改變設計實現。
class Logger { def log(msg) { println msg } }
class TimeStampingLogger extends Logger { ... }
class UpperLogger extends Logger { ... }
def logger = new UpperLogger(new TimeStampingLogger(new Logger()))
logger.log("Groovy Rocks") // 輸出帶時間戳的大寫
Groovy ExpandoMetaClass:
def logger = new Logger()
logger.metaClass.log = { String m -> println m.toUpperCase() }
logger.log("upper case") // 僅影響該實例
JRuby eigenclass:
l = Logger.new
def l.log(m); puts m.upcase; end
l.log("upper case")
無需額外類,修飾邏輯內聚在使用點,閱讀即理解。
Groovy:
ArrayList.metaClass.getFirst { delegate.size>0 ? delegate.get(0) : null }
ArrayList.metaClass.getLast { delegate.size>0 ? delegate.get(delegate.size-1) : null }
JRuby:
class ArrayList
def first; size!=0 ? get(0) : nil; end
def last; size!=0 ? get(size-1) : nil; end
end
Java 做不到不修改源碼或字節碼織入。
Groovy invokeMethod 鉤子:
class GenericLowerDecorator {
private delegate
def invokeMethod(String name, args) {
def newArgs = args.collect { it instanceof String ? it.toLowerCase() : it }
delegate.invokeMethod(name, newArgs)
}
}
logger = new GenericLowerDecorator(new TimeStampingLogger(new Logger()))
logger.log('IMPORTANT Message') // 參數變小寫,時間戳不變
Java 需 AspectJ 等 AOP 框架才能達到同等效果。
interface RoundThing { def getRadius() }
class SquarePeg { def width }
class RoundHole { def radius; def pegFits(peg) { peg.radius <= radius } }
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)
assert (w<6) == hole.pegFits(adapter(peg))
}
as 將 Map 動態生成實現介面的代理類,方法查找 Map 鍵值執行閉包。
class SquarePeg
def radius; Math.sqrt(((@width/2)**2)*2); end
end
hole = RoundHole.new(4.0)
4.upto(7) { |i| peg = SquarePeg.new(i); assert (i<6) == hole.peg_fits?(peg) }
若 SquarePeg 已有 radius 會衝突。
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(w); set_interface :square; @width=w; end
end
peg = SquarePeg.new(5)
peg.with_interface(:holes) { hole.peg_fits?(peg) } // 作用域內使用 holes 版本 radius
利用 Ruby 解釋期執行特性,為方法建立多個版本並通過作用域切換。Java 編譯期無法實現。