鍍金池/ 教程/ Java/ 設(shè)計(jì)模式
Grape 依賴管理器
與 Java 的區(qū)別
語(yǔ)法風(fēng)格指南
Groovy 開(kāi)發(fā)工具包
領(lǐng)域?qū)S谜Z(yǔ)言
安全更新
Groovy 與應(yīng)用的集成
運(yùn)行時(shí)及編譯時(shí)元編程(end)
測(cè)試指南
安裝 Groovy
設(shè)計(jì)模式
Groovy 的下載

設(shè)計(jì)模式

在 Java 中使用設(shè)計(jì)模式已成為一種成熟的主題。設(shè)計(jì)模式也適用于 Groovy:

  • 一些設(shè)計(jì)模式可以直接沿用(可以利用通常的 Groovy 語(yǔ)法提升特性來(lái)實(shí)現(xiàn)更好的可讀性)。
  • 有些設(shè)計(jì)模式變得沒(méi)有用了,因?yàn)樗鼈円呀?jīng)是語(yǔ)言內(nèi)部的組成部分,或者 Groovy 已經(jīng)找到了更好的辦法來(lái)達(dá)到它們的目的。
  • 對(duì)于有些在其他語(yǔ)言中必須在設(shè)計(jì)層面上表達(dá)的模式而言,Groovy 可以直接將其實(shí)現(xiàn)(因?yàn)?Groovy 可以消弭設(shè)計(jì)與實(shí)現(xiàn)之間的差異)。

1. 模式詳述

1.1 抽象工廠模式

抽象工廠模式能將一組有共同主題的獨(dú)立工廠封裝起來(lái)。它將普通工廠的意圖具體化,例如:不必再利用代碼通過(guò)使用接口來(lái)了解接口背后的具體實(shí)施,而應(yīng)用于一組接口,并且選擇一整套能夠?qū)崿F(xiàn)這些接口的具體類(lèi)。

考慮這樣一個(gè)范例,其中有接口 Button、TextField 和 Scrollbar。將 WindowsButton、MacButton、FlashButton 作為 Button 的具體類(lèi)。WindowsScrollBar、MacScrollBar 和 FlashScrollBar 作為 ScrollBar 的具體實(shí)現(xiàn)。抽象工廠模式應(yīng)能讓我選擇某次想要用到的具體窗口系統(tǒng)(比如 Windows、Mac、Flash),并從此可以編寫(xiě)代碼引用這些接口,但能一直在后臺(tái)使用正確的具體類(lèi)(來(lái)自某個(gè)窗口系統(tǒng))。

1.1.1 范例

假設(shè)我們要寫(xiě)一個(gè)游戲系統(tǒng),可能會(huì)注意到很多游戲其實(shí)都有非常一致的功能與控制方式。

于是我們打算把通用和游戲?qū)S械拇a分別寫(xiě)到不同的類(lèi)中。

首先來(lái)看看游戲 Two-Up(一種投擲雙幣賭博的游戲)專(zhuān)有代碼 :

class TwoupMessages {
    def welcome = 'Welcome to the twoup game, you start with $1000'
    def done = 'Sorry, you have no money left, goodbye'
}

class TwoupInputConverter {
    def convert(input) { input.toInteger() }
}

class TwoupControl {
    private money = 1000
    private random = new Random()
    private tossWasHead() {
        def next = random.nextInt()
        return next % 2 == 0
    }
    def moreTurns() {
        if (money > 0) {
            println "You have $money, how much would you like to bet?"
            return true
        }

        false
    }
    def play(amount) {
        def coin1 = tossWasHead()
        def coin2 = tossWasHead()
        if (coin1 && coin2) {
            money += amount
            println 'You win'
        } else if (!coin1 && !coin2) {
            money -= amount
            println 'You lose'
        } else {
            println 'Draw'
        }
    }
}

下面再來(lái)看看猜數(shù)字游戲的游戲?qū)S写a:

class GuessGameMessages {
    def welcome = 'Welcome to the guessing game, my secret number is between 1 and 100'
    def done = 'Correct'
}

class GuessGameInputConverter {
    def convert(input) { input.toInteger() }
}

class GuessGameControl {
    private lower = 1
    private upper = 100
    private guess = new Random().nextInt(upper - lower) + lower
    def moreTurns() {
        def done = (lower == guess || upper == guess)
        if (!done) {
            println "Enter a number between $lower and $upper"
        }

        !done
    }
    def play(nextGuess) {
        if (nextGuess <= guess) {
            lower = [lower, nextGuess].max()
        }
        if (nextGuess >= guess) {
            upper = [upper, nextGuess].min()
        }
    }
}

下面再來(lái)寫(xiě)工廠代碼:

def guessFactory = [messages: GuessGameMessages, control: GuessGameControl, converter: GuessGameInputConverter]
def twoupFactory = [messages: TwoupMessages, control: TwoupControl, converter: TwoupInputConverter]

class GameFactory {
    def static factory
    def static getMessages() { return factory.messages.newInstance() }
    def static getControl() { return factory.control.newInstance() }
    def static getConverter() { return factory.converter.newInstance() }
}

工廠的重點(diǎn)在于允許選擇整組具體類(lèi)。

下面是對(duì)工廠的使用:

GameFactory.factory = twoupFactory
def messages = GameFactory.messages
def control = GameFactory.control
def converter = GameFactory.converter
println messages.welcome
def reader = new BufferedReader(new InputStreamReader(System.in))
while (control.moreTurns()) {
    def input = reader.readLine().trim()
    control.play(converter.convert(input))
}
println messages.done

第一行配置了所要使用哪一組具體游戲類(lèi)。其實(shí),通過(guò)像第一行中那樣使用 factory 屬性來(lái)確定所用類(lèi)組并不是很重要。通過(guò)其他方式也可以實(shí)現(xiàn)這一點(diǎn)。比如,我們可以詢問(wèn)用戶想玩的游戲,或者從環(huán)境設(shè)置中確定使用何種游戲。

根據(jù)以上的代碼,游戲運(yùn)行結(jié)果大概如下所示:

Welcome to the twoup game, you start with $1000
You have 1000, how much would you like to bet?
300
Draw
You have 1000, how much would you like to bet?
700
You win
You have 1700, how much would you like to bet?
1700
You lose
Sorry, you have no money left, goodbye

如果把第一行腳本改為:GameFactory.factory = guessFactory,那么范例運(yùn)行結(jié)果如下:

Welcome to the guessing game, my secret number is between 1 and 100
Enter a number between 1 and 100
75
Enter a number between 1 and 75
35
Enter a number between 1 and 35
15
Enter a number between 1 and 15
5
Enter a number between 5 and 15
10
Correct

1.2 適配器模式

適配器模式(有時(shí)叫做包裝模式)允許對(duì)象用在另一類(lèi)接口。這一模式有兩種

1.2.1 委托范例

假設(shè)有以下類(lèi):

class SquarePeg {
    def width
}

class RoundPeg {
    def radius
}

class RoundHole {
    def radius
    def pegFits(peg) {
        peg.radius <= radius
    }
    String toString() { "RoundHole with radius $radius" }
}

可以詢問(wèn) RoundHole 類(lèi)是否有 RoundPeg 適合它,但詢問(wèn) SquarePeg 相同的問(wèn)題則不會(huì)成功,因?yàn)?SquarePeg 根本就沒(méi)有 radius 屬性(比如并不滿足必須的接口)。

要想解決這個(gè)問(wèn)題,就需要?jiǎng)?chuàng)建一個(gè)適配器使其具有正確的接口,如下所示:

class SquarePegAdapter {
    def peg
    def getRadius() {
        Math.sqrt(((peg.width / 2) ** 2) * 2)
    }
    String toString() {
        "SquarePegAdapter with peg width $peg.width (and notional radius $radius)"
    }
}

也可以像下面這樣來(lái)使用適配器:

def hole = new RoundHole(radius: 4.0)
(4..7).each { w ->
    def peg = new SquarePegAdapter(peg: new SquarePeg(width: w))
    if (hole.pegFits(peg)) {
        println "peg $peg fits in hole $hole"
    } else {
        println "peg $peg does not fit in hole $hole"
    }
}

其返回結(jié)果如下:

peg SquarePegAdapter with peg width 4 (and notional radius 2.8284271247461903) fits in hole RoundHole with radius 4.0
peg SquarePegAdapter with peg width 5 (and notional radius 3.5355339059327378) fits in hole RoundHole with radius 4.0
peg SquarePegAdapter with peg width 6 (and notional radius 4.242640687119285) does not fit in hole RoundHole with radius 4.0
peg SquarePegAdapter with peg width 7 (and notional radius 4.949747468305833) does not fit in hole RoundHole with radius 4.0

1.2.2 繼承范例

下面使用繼承來(lái)實(shí)現(xiàn)同樣的范例。首先,原始類(lèi)如下(沒(méi)有任何變動(dòng)):

class SquarePeg {
    def width
}

class RoundPeg {
    def radius
}

class RoundHole {
    def radius
    def pegFits(peg) {
        peg.radius <= radius
    }
    String toString() { "RoundHole with radius $radius" }
}

使用繼承的適配器:

class SquarePegAdapter extends SquarePeg {
    def getRadius() {
        Math.sqrt(((width / 2) ** 2) * 2)
    }
    String toString() {
        "SquarePegAdapter with width $width (and notional radius $radius)"
    }
}

使用適配器:

def hole = new RoundHole(radius: 4.0)
(4..7).each { w ->
    def peg = new SquarePegAdapter(peg: new SquarePeg(width: w))
    if (hole.pegFits(peg)) {
        println "peg $peg fits in hole $hole"
    } else {
        println "peg $peg does not fit in hole $hole"
    }
}

輸出為:

peg SquarePegAdapter with width 4 (and notional radius 2.8284271247461903) fits in hole RoundHole with radius 4.0
peg SquarePegAdapter with width 5 (and notional radius 3.5355339059327378) fits in hole RoundHole with radius 4.0
peg SquarePegAdapter with width 6 (and notional radius 4.242640687119285) does not fit in hole RoundHole with radius 4.0
peg SquarePegAdapter with width 7 (and notional radius 4.949747468305833) does not fit in hole RoundHole with radius 4.0

1.2.3 使用閉包適配

作為以上范例的變體,我們可以定義下列接口:

interface RoundThing {
    def getRadius()
}

定義一個(gè)閉包作為適配器:

def adapter = {
    p -> [getRadius: { Math.sqrt(((p.width / 2) ** 2) * 2) }] as RoundThing
}

然后使用它:

def peg = new SquarePeg(width: 4)
if (hole.pegFits(adapter(peg))) {
    // ... 如前所示  
}

1.2.4. 使用 ExpandoMetaClass 來(lái)適配

自從 Groovy 1.1 起,就存在一個(gè)內(nèi)建的 MetaClass,能夠自動(dòng)且動(dòng)態(tài)地添加屬性和方法。

下例就用到了這個(gè)特性:

def peg = new SquarePeg(width: 4)
peg.metaClass.radius = Math.sqrt(((peg.width / 2) ** 2) * 2)

創(chuàng)建完 peg 對(duì)象之后,可以立即添加一個(gè)屬性。不需要改變?cè)碱?lèi),也不需要適配類(lèi)。

1.3 保鏢模式

保鏢模式(Bouncer Pattern)指的是某個(gè)方法唯一目的在于拋出異常(當(dāng)滿足特定條件時(shí))或什么都不做。這種方法經(jīng)常使用于防衛(wèi)警戒某方法的前提條件。

當(dāng)編寫(xiě)工具方法時(shí),應(yīng)對(duì)有可能會(huì)出毛病的輸入?yún)?shù)保持警惕。在編寫(xiě)內(nèi)部方法時(shí),應(yīng)該通過(guò)有效的單元測(cè)試來(lái)保證一直持有特定前提條件。因?yàn)樵谶@些情況下,人們可能不會(huì)注重方法的防御。

在這一點(diǎn)上,Groovy 區(qū)別于其他語(yǔ)言的地方就在于,經(jīng)常在方法中使用 assert 方法,而不是大量的工具檢查器方法或類(lèi)。

1.3.1 Null 檢查范例

假如工具方法如下所示:

class NullChecker {
    static check(name, arg) {
        if (arg == null) {
            throw new IllegalArgumentException(name + ' is null')
        }
    }
}

它的用法可能如下:

void doStuff(String name, Object value) {
    NullChecker.check('name', name)
    NullChecker.check('value', value)
    // 事務(wù)邏輯   
}

但用 Groovy 來(lái)做,就簡(jiǎn)單多了:

void doStuff(String name, Object value) {
    assert name != null, 'name should not be null'
    assert value != null, 'value should not be null'
    // 事務(wù)邏輯   
}

1.3.2 驗(yàn)證范例

作為一種替代性范例,可能會(huì)有下面這樣的工具方法:

class NumberChecker {
    static final String NUMBER_PATTERN = "\\\\d+(\\\\.\\\\d+(E-?\\\\d+)?)?"
    static isNumber(str) {
        if (!str ==~ NUMBER_PATTERN) {
            throw new IllegalArgumentException("Argument '$str' must be a number")
        }
    }
    static isNotZero(number) {
        if (number == 0) {
            throw new IllegalArgumentException('Argument must not be 0')
        }
    }
}

并像下面這樣使用它:

def stringDivide(String dividendStr, String divisorStr) {
    NumberChecker.isNumber(dividendStr)
    NumberChecker.isNumber(divisorStr)
    def dividend = dividendStr.toDouble()
    def divisor = divisorStr.toDouble()
    NumberChecker.isNotZero(divisor)
    dividend / divisor
}

println stringDivide('1.2E2', '3.0')
// => 40.0

但利用 Groovy,我們只需這樣使用即可:

def stringDivide(String dividendStr, String divisorStr) {
    assert dividendStr =~ NumberChecker.NUMBER_PATTERN
    assert divisorStr =~ NumberChecker.NUMBER_PATTERN
    def dividend = dividendStr.toDouble()
    def divisor = divisorStr.toDouble()
    assert divisor != 0, 'Divisor must not be 0'
    dividend / divisor
}

1.4 職責(zé)鏈模式

在職責(zé)鏈模式下,有意將使用并實(shí)現(xiàn)一個(gè)接口(一個(gè)或更多的方法)的數(shù)個(gè)對(duì)象松散耦合。一組實(shí)現(xiàn)接口的對(duì)象按照列表的形式進(jìn)行組合(或者在極少數(shù)情況下按照樹(shù)的形式)。使用接口的對(duì)象向第一個(gè)實(shí)現(xiàn)器implementor)對(duì)象發(fā)出請(qǐng)求。第一個(gè)實(shí)現(xiàn)器對(duì)象會(huì)確定是否自己執(zhí)行任何行為,以及是否將該請(qǐng)求沿列表(或樹(shù))傳播下去。有時(shí),當(dāng)實(shí)現(xiàn)器對(duì)象都沒(méi)有響應(yīng)請(qǐng)求時(shí),會(huì)在模式中編寫(xiě)對(duì)一些請(qǐng)求的默認(rèn)實(shí)現(xiàn)。

1.4.1 范例

在該例中,腳本向 lister 對(duì)象發(fā)送請(qǐng)求,lister 指向 UnixLister 對(duì)象。如果它無(wú)法處理該請(qǐng)求,則將該請(qǐng)求發(fā)送給 WindowsLister。如果還不能處理該請(qǐng)求,則繼續(xù)發(fā)送給 DefaultLister。

class UnixLister {
    private nextInLine
    UnixLister(next) { nextInLine = next }
    def listFiles(dir) {
        if (System.getProperty('os.name') == 'Linux') {
            println "ls $dir".execute().text
        } else {
            nextInLine.listFiles(dir)
        }
    }
}

class WindowsLister {
    private nextInLine
    WindowsLister(next) { nextInLine = next }
    def listFiles(dir) {
        if (System.getProperty('os.name') == 'Windows XP') {
            println "cmd.exe /c dir $dir".execute().text
        } else {
            nextInLine.listFiles(dir)
        }
    }
}

class DefaultLister {
    def listFiles(dir) {
        new File(dir).eachFile { f -> println f }
    }
}

def lister = new UnixLister(new WindowsLister(new DefaultLister()))

lister.listFiles('Downloads')

輸出一個(gè)文件列表(根據(jù)操作系統(tǒng)的差別,輸出格式將稍有不同)。

下面是 UML 的表示形式:

http://wiki.jikexueyuan.com/project/groovy-introduction/images/ChainOfResponsibilityClasses.gif" alt="ChainOfResponsibilityClasses" />

該模式的變體形式有:

  • 可以利用顯式接口(比如 Lister)將實(shí)現(xiàn)進(jìn)行靜態(tài)類(lèi)型轉(zhuǎn)換,但由于 duck-typing 特性的存在,這是一種選擇方法。

  • 可以使用鏈樹(shù)而非列表 if (animal.hasBackbone()) 委托給 VertebrateHandler,否則就委托給 InvertebrateHandler。

  • 可以一直沿鏈傳遞,即使在處理請(qǐng)求時(shí)。

  • 可以在一定的時(shí)間不響應(yīng)不再沿鏈進(jìn)行傳遞。

  • 使用 Groovy 的元編程特性沿鏈傳遞未知方法。

1.5 復(fù)合模式

復(fù)合模式(Composite Pattern)下,一組對(duì)象與單個(gè)對(duì)象的多個(gè)實(shí)例在處理方式上是相同的。該模式常用于對(duì)象層級(jí)。通常,對(duì)于層級(jí)中的葉(leaf)或復(fù)合(composite)節(jié)點(diǎn),一個(gè)或多個(gè)方法的調(diào)用方式應(yīng)該是相同的。在這種情況下,復(fù)合節(jié)點(diǎn)通常會(huì)調(diào)用它每一個(gè)子節(jié)點(diǎn)的同一命名方法。

1.5.1 范例

下面探討一下復(fù)合模式的一個(gè)范例,在 LeafComposite 對(duì)象上調(diào)用 toString()

http://wiki.jikexueyuan.com/project/groovy-introduction/images/CompositeClasses.gif" alt="CompositeClasses" />

在 Java 中,Component 類(lèi)是非常重要的,因?yàn)樗峁┝巳~節(jié)點(diǎn)與復(fù)合節(jié)點(diǎn)所用的類(lèi)型。在 Groovy 中,由于 duck-typing,所以不再需要用它來(lái)完成這種用途,但該類(lèi)依然非常有用,可以在葉節(jié)點(diǎn)與復(fù)合節(jié)點(diǎn)間放置常見(jiàn)的行為。

根據(jù)目標(biāo),我們將構(gòu)建以下的層級(jí):

http://wiki.jikexueyuan.com/project/groovy-introduction/images/CompositeComponents.gif" alt="CompositeComponents" />

代碼如下:

abstract class Component {
    def name
    def toString(indent) {
        ("-" * indent) + name
    }
}

class Composite extends Component {
    private children = []
    def toString(indent) {
        def s = super.toString(indent)
        children.each { child ->
            s += "\\n" + child.toString(indent + 1)
        }
        s
    }
    def leftShift(component) {
        children << component
    }
}

class Leaf extends Component { }

def root = new Composite(name: "root")
root << new Leaf(name: "leaf A")
def comp = new Composite(name: "comp B")
root << comp
root << new Leaf(name: "leaf C")
comp << new Leaf(name: "leaf B1")
comp << new Leaf(name: "leaf B2")
println root.toString(0)

結(jié)果輸出如下:

root
-leaf A
-comp B
--leaf B1
--leaf B2
-leaf C

1.6 裝飾器模式

裝飾器模式(Decorator Pattern)可以在不改變一個(gè)對(duì)象的基本接口的情況裝飾其行為。當(dāng)需要原始對(duì)象(未被裝飾對(duì)象)時(shí),被裝飾對(duì)象可以被替換。裝飾行為通常并不會(huì)包含對(duì)原始對(duì)象源碼的修改,裝飾器應(yīng)該能以各種靈活的手段進(jìn)行組合,從而形成帶有多種裝飾行為的對(duì)象。

1.6.1 典型范例

假設(shè)現(xiàn)在有一個(gè) Logger 類(lèi):

class Logger {
    def log(String message) {
        println message
    }
}

有時(shí)為日志記錄添加時(shí)間戳是非常有用的,或者可能想改變消息的大小寫(xiě)格式。我們可能會(huì)試圖將所有的功能都塞進(jìn) Logger 類(lèi)中。這會(huì)使得 Logger 類(lèi)變得十分復(fù)雜臃腫。另外,每一個(gè)人都將獲得所有的功能,即使你不想要其中的某個(gè)小子集,也毫無(wú)辦法。最后,功能交互就會(huì)變得極難控制。

為了克服這些缺點(diǎn),定義兩個(gè)裝飾器類(lèi)。Logger 類(lèi)的用途在于可自由地裝飾它們的基本記錄器,其中可按照任何預(yù)想順序用到多個(gè)(或不用)裝飾器類(lèi)。這樣的類(lèi)如下所示:

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())
    }
}

可以像下面這樣使用裝飾器:

def logger = new UpperLogger(new TimeStampingLogger(new Logger()))
logger.log("G'day Mate")
// => Tue May 22 07:13:50 EST 2007: G'DAY MATE

由上可見(jiàn),我們用了兩個(gè)裝飾器來(lái)裝飾 logger 的行為。根據(jù)我們選用的裝飾器使用順序,日志消息最終結(jié)果是全部大寫(xiě)字母,而時(shí)間戳則還是正常的格式。如果我們交換順序,則結(jié)果如下:

logger = new TimeStampingLogger(new UpperLogger(new Logger()))
logger.log('Hi There')
// => TUE MAY 22 07:13:50 EST 2007: HI THERE

注意時(shí)間戳本身也被改成了大寫(xiě)。

1.6.2 探討一下動(dòng)態(tài)行為

之前的裝飾器都是 Logger 類(lèi)所特有的。我們可以利用 Groovy 的元編程特性創(chuàng)建一個(gè)裝飾器,使其自然具有更通用的功用,比如像下面這個(gè)類(lèi):

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)
    }
}

它將接受任何類(lèi)并對(duì)其進(jìn)行裝飾,從而使任何 String 方法的參數(shù)都能自動(dòng)變更為小寫(xiě)。

logger = new GenericLowerDecorator(new TimeStampingLogger(new Logger()))
logger.log('IMPORTANT Message')
// => Tue May 22 07:27:18 EST 2007: important message

只是要注意這里的順序。原始的裝飾器被局限于只能裝飾 Logger 對(duì)象。這個(gè)裝飾器適用于任何對(duì)象類(lèi)型,所以不能將順序調(diào)換,比如不能像下面這樣:

// Can't mix and match Interface-Oriented and Generic decorators
// logger = new TimeStampingLogger(new GenericLowerDecorator(new Logger()))

在運(yùn)行時(shí)生成一個(gè)正確的 Proxy 類(lèi)型,就可以克服這個(gè)限制,但在這個(gè)例子中,我們不想搞得那么復(fù)雜。

1.6.3 運(yùn)行時(shí)行為裝飾

你現(xiàn)在可能還在認(rèn)為,像從 Groovy 1.1 開(kāi)始一直所采用的那樣,利用ExpandoMetaClass 來(lái)動(dòng)態(tài)裝飾類(lèi)的行為。這雖然不是裝飾器模式通常的風(fēng)格(當(dāng)然談不上有多靈活),但卻可能在有些場(chǎng)合幫你實(shí)現(xiàn)相似的結(jié)果,而無(wú)需創(chuàng)建新類(lèi)。

下面是范例代碼:

// ExpandoMetaClass 的當(dāng)前使用機(jī)制
GroovySystem.metaClassRegistry.metaClassCreationHandle = new ExpandoMetaClassCreationHandle()

def logger = new Logger()
logger.metaClass.log = { String m -> println 'message: ' + m.toUpperCase() }
logger.log('x')
// => message: X

這樣做雖然也和應(yīng)用了一個(gè)單獨(dú)的裝飾器的效果相似,但卻無(wú)法輕松地立刻使用并去除裝飾。

1.6.4 更多動(dòng)態(tài)裝飾

假設(shè)有一個(gè)計(jì)算器類(lèi)(實(shí)際上任何類(lèi)都可以當(dāng)做范例)。

class Calc {
    def add(a, b) { a + b }
}

我們可能會(huì)需要觀察類(lèi)在一段時(shí)間的使用情況。如果它深埋在代碼基中,可能很難判斷它是何時(shí)調(diào)用的以及所用的參數(shù)。另外,也很難知道它是否成功執(zhí)行。我們可以輕松地創(chuàng)建一個(gè)通用的跟蹤裝飾器,只要 Calc 類(lèi)上的任何方法一被調(diào)用,就讓它打印出跟蹤信息,以及提供方法執(zhí)行的時(shí)間信息。下面就是這個(gè)跟蹤裝飾器的代碼:

class TracingDecorator {
    private delegate
    TracingDecorator(delegate) {
        this.delegate = delegate
    }
    def invokeMethod(String name, args) {
        println "Calling $name$args"
        def before = System.currentTimeMillis()
        def result = delegate.invokeMethod(name, args)
        println "Got $result in ${System.currentTimeMillis()-before} ms"
        result
    }
}

下面是如何在腳本中使用這個(gè)類(lèi):

def tracedCalc = new TracingDecorator(new Calc())
assert 15 == tracedCalc.add(3, 12)

運(yùn)行該腳本,所得結(jié)果如下:

Calling add{3, 12}
Got 15 in 31 ms

1.6.5 利用攔截器進(jìn)行裝飾

上面的計(jì)時(shí)范例與 Groovy 對(duì)象的生命周期(通過(guò) invokeMethod)。這是一種實(shí)現(xiàn)元編程的非常重要的方式,以至于 Groovy 為這一使用攔截器interceptors)的裝飾方式提供了特殊支持。

Groovy 甚至還內(nèi)建了 TracingInterceptor。還可以像下面這樣擴(kuò)展內(nèi)建類(lèi):

class TimingInterceptor extends TracingInterceptor {
    private beforeTime
    def beforeInvoke(object, String methodName, Object[] arguments) {
        super.beforeInvoke(object, methodName, arguments)
        beforeTime = System.currentTimeMillis()
    }
    Object afterInvoke(Object object, String methodName, Object[] arguments, Object result) {
        super.afterInvoke(object, methodName, arguments, result)
        def duration = System.currentTimeMillis() - beforeTime
        writer.write("Duration: $duration ms\\n")
        writer.flush()
        result
    }
}

有關(guān)于這個(gè)新類(lèi),使用范例如下所示:

def proxy = ProxyMetaClass.getInstance(Calc)
proxy.interceptor = new TimingInterceptor()
proxy.use {
    assert 7 == new Calc().add(1, 6)
}

輸出結(jié)果如下:

before Calc.ctor()
after  Calc.ctor()
Duration: 0 ms
before Calc.add(java.lang.Integer, java.lang.Integer)
after  Calc.add(java.lang.Integer, java.lang.Integer)
Duration: 2 ms

1.6.6 利用 java.lang.reflect.Proxy 進(jìn)行裝飾

如果想裝飾一個(gè)對(duì)象(比如只是某個(gè)類(lèi)的一個(gè)特殊實(shí)例,而不是類(lèi)本身),可以使用 Java 的 java.lang.reflect.Proxy。Groovy 這樣做起來(lái)要更簡(jiǎn)單一些。下面這個(gè)代碼范例取自一個(gè) grails 項(xiàng)目,它封裝了 java.sql.Connection,所以它的 close 方法是無(wú)參的。

protected Sql getGroovySql() {
    final Connection con = session.connection()
    def invoker = { object, method, args ->
        if (method.name == "close") {
            log.debug("ignoring call to Connection.close() for use by groovy.sql.Sql")
        } else {
            log.trace("delegating $method")
            return con.invokeMethod(method.name, args)
        }
    } as InvocationHandler;
    def proxy = Proxy.newProxyInstance( getClass().getClassLoader(), [Connection] as Class[], invoker )
    return new Sql(proxy)
}

如果有很多方法需要攔截,那么經(jīng)過(guò)修改后,該方法可以按照方法名查找映射中的閉包,并加以調(diào)用。

1.6.7 利用 Spring 進(jìn)行裝飾

Spring 框架允許利用攔截器來(lái)應(yīng)用裝飾器(你可能聽(tīng)過(guò)名詞 advice 或 aspect)。也可以用 Groovy 來(lái)實(shí)現(xiàn)這種機(jī)制。

首先定義一個(gè)需要裝飾的類(lèi)(另外也會(huì)用到一個(gè)接口,因?yàn)檫@是 Spring 的慣例)。

假設(shè)該接口如下:

interface Calc {
    def add(a, b)
}

類(lèi)如下:

class CalcImpl implements Calc {
    def add(a, b) { a + b }
}

下面,在一個(gè) beans.xml 的文件中定義我們的連線:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:lang="http://www.springframework.org/schema/lang"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd">

    <bean id="performanceInterceptor" autowire="no"
          class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor">
        <property name="loggerName" value="performance"/>
    </bean>
    <bean id="calc" class="util.CalcImpl"/>
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="beanNames" value="calc"/>
        <property name="interceptorNames" value="performanceInterceptor"/>
    </bean>
</beans>

腳本如下:

@Grab('org.springframework:spring-context:3.2.2.RELEASE')
import org.springframework.context.support.ClassPathXmlApplicationContext

def ctx = new ClassPathXmlApplicationContext('beans.xml')
def calc = ctx.getBean('calc')
println calc.add(3, 25)

運(yùn)行腳本的結(jié)果如下:

21/05/2007 23:02:35 org.springframework.aop.interceptor.PerformanceMonitorInterceptor invokeUnderTrace
FINEST: StopWatch 'util.Calc.add': running time (millis) = 16

為了顯示在日志級(jí)別 FINEST 的消息,我們必須調(diào)整 logging.properties 文件。

1.6.8 使用 GPars 的異步裝飾者

Panini 上的代碼范例有一定的啟發(fā)性。避免使用 @AddedBehavior 標(biāo)記。

@Grab('org.codehaus.gpars:gpars:0.10')
import static groovyx.gpars.GParsPool.withPool

interface Document {
    void print()
    String getText()
}

class DocumentImpl implements Document {
    def document
    void print() { println document }
    String getText() { document }
}

def words(String text) {
    text.replaceAll('[^a-zA-Z]', ' ').trim().split("\\\\s+")*.toLowerCase()
}

def avgWordLength = {
    def words = words(it.text)
    sprintf "Avg Word Length: %4.2f", words*.size().sum() / words.size()
}
def modeWord = {
    def wordGroups = words(it.text).groupBy {it}.collectEntries { k, v -> [k, v.size()] }
    def maxSize = wordGroups*.value.max()
    def maxWords = wordGroups.findAll { it.value == maxSize }
    "Mode Word(s): ${maxWords*.key.join(', ')} ($maxSize occurrences)"
}
def wordCount = { d -> "Word Count: " + words(d.text).size() }

def asyncDecorator(Document d, Closure c) {
    ProxyGenerator.INSTANCE.instantiateDelegate([print: {
        withPool {
            def result = c.callAsync(d)
            d.print()
            println result.get()
        }
    }], [Document], d)
}

Document d = asyncDecorator(asyncDecorator(asyncDecorator(
        new DocumentImpl(document:"This is the file with the words in it\\n\\t\\nDo you see the words?\\n"),
//        new DocumentImpl(document: new File('AsyncDecorator.groovy').text),
        wordCount), modeWord), avgWordLength)
d.print()

1.7 委托模式

委托模式(Delegation Pattern)對(duì)象行為(公共方法)由委托的一個(gè)或多個(gè)對(duì)象實(shí)現(xiàn)。

Groovy 還允許使用委托模式的傳統(tǒng)方式,比如用委托代替繼承

1.7.1 利用 ExpandoMetaClass 實(shí)現(xiàn)委托模式

groovy.lang.ExpandoMetaClass 允許通過(guò)庫(kù)封裝來(lái)使用該模式。這能讓 Groovy 效仿 Ruby 語(yǔ)言所用的類(lèi)似庫(kù)。

考慮下面這個(gè)庫(kù)類(lèi):

class Delegator {
    private targetClass
    private delegate
    Delegator(targetClass, delegate) {
        this.targetClass = targetClass
        this.delegate = delegate
    }
    def delegate(String methodName) {
        delegate(methodName, methodName)
    }
    def delegate(String methodName, String asMethodName) {
        targetClass.metaClass."$asMethodName" = delegate.&"$methodName"
    }
    def delegateAll(String[] names) {
        names.each { delegate(it) }
    }
    def delegateAll(Map names) {
        names.each { k, v -> delegate(k, v) }
    }
    def delegateAll() {
        delegate.class.methods*.name.each { delegate(it) }
    }
}

將它放在類(lèi)路徑中,可以像上例所示的那樣動(dòng)態(tài)地應(yīng)用委托模式。首先假設(shè)我們有以下這些類(lèi):

class Person {
    String name
}

class MortgageLender {
    def borrowAmount(amount) {
       "borrow \\$$amount"
    }
    def borrowFor(thing) {
       "buy \\$thing"
    }
}

def lender = new MortgageLender()

def delegator = new Delegator(Person, lender)

可以使用 delegator 自動(dòng)地從 lender 對(duì)象處借調(diào)方法來(lái)擴(kuò)展 Person 類(lèi)。我們可以照原樣借調(diào)方法,也可以對(duì)方法改名。

delegator.delegate 'borrowFor'
delegator.delegate 'borrowAmount', 'getMoney'

def p = new Person()

println p.borrowFor('present')   // => buy present
println p.getMoney(50)

上述代碼中的第一行中,通過(guò)委托給 lender 對(duì)象,為 Person 類(lèi)添加了 borrowFor 方法。第二行中,通過(guò)委托給 lender 對(duì)象的 borrowAmount 方法,為 Person 類(lèi)添加了一個(gè) getMoney 方法。

另外,我們還可以借調(diào)多個(gè)方法:

delegator.delegateAll 'borrowFor', 'borrowAmount'

這會(huì)將兩種方法都添加到 Person 類(lèi)上。

或者,有時(shí)我們會(huì)需要所有方法:

delegator.delegateAll()  

從而使 Person 類(lèi)能夠使用委托對(duì)象中的所有方法。

另外,可以利用映射標(biāo)記重新命名多個(gè)方法:

delegator.delegateAll borrowAmount:'getMoney', borrowFor:'getThing'  

1.7.2 利用 @Delegate 標(biāo)記實(shí)現(xiàn)委托模式

從 1.6 版開(kāi)始,可以使用內(nèi)建的基于 AST 變換的委托模式。

委托可以變得更簡(jiǎn)單:

class Person {
    def name
    @Delegate MortgageLender mortgageLender = new MortgageLender()
}

class MortgageLender {
    def borrowAmount(amount) {
       "borrow \\$$amount"
    }
    def borrowFor(thing) {
       "buy $thing"
    }
}

def p = new Person()

assert "buy present" == p.borrowFor('present')
assert "borrow \\$50" == p.borrowAmount(50)

1.8 享元模式

利用享元模式(Flyweight Pattern)能夠極大減少內(nèi)存需求,因?yàn)樗梢员苊庠谔幚戆芏鄻O其相似部分的系統(tǒng)時(shí),大量地去創(chuàng)建重量級(jí)對(duì)象。例如,利用一個(gè)復(fù)雜的字符類(lèi)來(lái)構(gòu)建文檔,該字符類(lèi)要處理unicode、字體以及定位等情況。文檔越大,如果文檔中的每一個(gè)實(shí)際字符都需要一個(gè)特有的字符類(lèi)實(shí)例的話,內(nèi)存需求顯然就會(huì)越大。相反,由于字符本身可能保存在字符串中,我們可能僅僅會(huì)用到單個(gè)的了解字符處理規(guī)范的字符類(lèi)(或少量的字符類(lèi),比如一個(gè)字符類(lèi)對(duì)應(yīng)一個(gè)字體類(lèi)型)。

在這種情境下,我們把這種由多個(gè)事物(比如字符類(lèi)型)所共享的狀態(tài)定義為內(nèi)部instrinsic)狀態(tài)。它被重量級(jí)類(lèi)所捕獲。區(qū)分實(shí)際字符(可能只是它的 ASCII 碼或 Unicode)的狀態(tài)被稱為外部extrinsic)狀態(tài)。

1.8.1 范例

首先我們構(gòu)建一些較為復(fù)雜的飛機(jī)(第一種飛機(jī)與第二種存在一定的競(jìng)爭(zhēng)關(guān)系,但有這一點(diǎn)與范例毫無(wú)關(guān)系):

class Boeing797 {
    def wingspan = '80.8 m'
    def capacity = 1000
    def speed = '1046 km/h'
    def range = '14400 km'
    // ...
}

http://wiki.jikexueyuan.com/project/groovy-introduction/images/b797-hoax.jpg" alt="b797-hoax" />

class Airbus380 {
    def wingspan = '79.8 m'
    def capacity = 555
    def speed = '912 km/h'
    def range = '10370 km'
    // ...
}

http://wiki.jikexueyuan.com/project/groovy-introduction/images/a380.jpg" alt="a380" />

如果想打造自己的機(jī)隊(duì),我們首先可能會(huì)去使用這些重量級(jí)類(lèi)的很多實(shí)例。但在實(shí)際情況下,各架飛機(jī)可能只有很小的狀態(tài)(外部狀態(tài))改動(dòng),所以我們還是借助重量級(jí)對(duì)象的單例,分別捕獲外部狀態(tài)即可(購(gòu)買(mǎi)日期與資產(chǎn)編號(hào),具體可參見(jiàn)下面的代碼):

class FlyweightFactory {
    static instances = [797: new Boeing797(), 380: new Airbus380()]
}

class Aircraft {
    private type         // 內(nèi)部狀態(tài)   
    private assetNumber  // 外部狀態(tài)   
    private bought       // 外部狀態(tài)   
    Aircraft(typeCode, assetNumber, bought) {
        type = FlyweightFactory.instances[typeCode]
        this.assetNumber = assetNumber
        this.bought = bought
    }
    def describe() {
        println """
        Asset Number: $assetNumber
        Capacity: $type.capacity people
        Speed: $type.speed
        Range: $type.range
        Bought: $bought
        """
    }
}

def fleet = [
    new Aircraft(380, 1001, '10-May-2007'),
    new Aircraft(380, 1002, '10-Nov-2007'),
    new Aircraft(797, 1003, '10-May-2008'),
    new Aircraft(797, 1004, '10-Nov-2008')
]

fleet.each { p -> p.describe() }

所以,按照這種方法,假如機(jī)隊(duì)有幾百家飛機(jī)的話,我們完全可以讓一個(gè)重量級(jí)對(duì)象來(lái)對(duì)應(yīng)一種類(lèi)型的飛機(jī)。

但為了更進(jìn)一步的高效率訴求,我們不再預(yù)先創(chuàng)建初始映射,而可能只延遲使用享元對(duì)象。

上面腳本運(yùn)行結(jié)果為:

Asset Number: 1001
Capacity: 555 people
Speed: 912 km/h
Range: 10370 km
Bought: 10-May-2007

Asset Number: 1002
Capacity: 555 people
Speed: 912 km/h
Range: 10370 km
Bought: 10-Nov-2007

Asset Number: 1003
Capacity: 1000 people
Speed: 1046 km/h
Range: 14400 km
Bought: 10-May-2008

Asset Number: 1004
Capacity: 1000 people
Speed: 1046 km/h
Range: 14400 km
Bought: 10-Nov-2008

1.9 迭代器模式

迭代器模式不暴露底層表達(dá)的情況下,連續(xù)訪問(wèn)聚合對(duì)象的元素。

Groovy 在很多閉包操作符中內(nèi)建有迭代器模式,比如 eacheachWithIndex,以及 for .. in 循環(huán)。

例如:

def printAll(container) {
    for (item in container) { println item }
}

def numbers = [ 1,2,3,4 ]
def months = [ Mar:31, Apr:30, May:31 ]
def colors = [ java.awt.Color.BLACK, java.awt.Color.WHITE ]
printAll numbers
printAll months
printAll colors

輸出結(jié)果如下:

1
2
3
4
May=31
Mar=31
Apr=30
java.awt.Color[r=0,g=0,b=0]
java.awt.Color[r=255,g=255,b=255]

另一個(gè)范例:

colors.eachWithIndex { item, pos ->
    println "Position $pos contains '$item'"
}

結(jié)果為:

Position 0 contains 'java.awt.Color[r=0,g=0,b=0]'
Position 1 contains 'java.awt.Color[r=255,g=255,b=255]'

另外,在其他一些專(zhuān)有操作符中也內(nèi)建了迭代器模式,這些操作符包括:eachByte、eachFileeachDir、eachLineeachObjecteachMatch,以便用于處理流、URL、文件、目錄以及正則表達(dá)式的匹配。

1.10 貸出資源模式

貸出資源模式(Loan my Resource) 用于確保資源一旦超出范圍后,就能得到處置。

許多 Groovy 的輔助方法都內(nèi)建有這種模式。如果需要處理 Groovy 支持范圍之外的資源,就應(yīng)該考慮使用這一模式。

1.10.1 范例

下面這個(gè)范例代碼用于處理文件。首先寫(xiě)入一些代碼,然后打印該文件的尺寸:

def f = new File('junk.txt')
f.withPrintWriter { pw ->
    pw.println(new Date())
    pw.println(this.class.name)
}
println f.size()
// => 42

下面讀取文件內(nèi)容,每次讀取并打印一行內(nèi)容。

f.eachLine { line ->
    println line
}
// =>
// Mon Jun 18 22:38:17 EST 2007
// RunPattern

注意,Groovy 其實(shí)在幕后使用了Java 中常見(jiàn)的 ReaderPrintWriter 對(duì)象,開(kāi)發(fā)者不用擔(dān)心如何顯式地創(chuàng)建或關(guān)閉這些資源。內(nèi)建的 Groovy 方法向閉包代碼貸出相應(yīng)的 reader 或 writer 對(duì)象,然后再進(jìn)行自我整理,因而無(wú)需進(jìn)行任何額外的操作。

但是,相對(duì)于使用 Groovy 內(nèi)建機(jī)制所達(dá)成的結(jié)果,你可能更希望實(shí)現(xiàn)一些稍微不同的結(jié)果。所以你應(yīng)該考慮在你自己的資源處理操作中使用該模式。

下面來(lái)考慮如何處理文件中每行中的單詞列表。可以使用 Groovy 內(nèi)建的函數(shù)來(lái)實(shí)現(xiàn),但請(qǐng)忍耐一下,假設(shè)我們可以自己來(lái)處理一些資源。在不使用該模式的情況下進(jìn)行編碼:

def reader = f.newReader()
reader.splitEachLine(' ') { wordList ->
    println wordList
}
reader.close()
// =>
// [ "Mon", "Jun", "18", "22:38:17", "EST", "2007" ]
// [ "RunPattern" ]

注意在代碼中我們顯式地調(diào)用了 close()。假如我們沒(méi)有正確地編碼(這里沒(méi)有用 try …? finally 代碼塊包圍代碼),就會(huì)有將文件處理公開(kāi)的危險(xiǎn)性。

下面使用貸出模式,首先編寫(xiě)一個(gè)輔助方法:

def withListOfWordsForEachLine(File f, Closure c) {
    def r = f.newReader()
    try {
        r.splitEachLine(' ', c)
    } finally {
        r?.close()
    }
}

然后重寫(xiě)代碼,如下所示:

withListOfWordsForEachLine(f) { wordList ->
    println wordList
}
// =>
// [ "Mon", "Jun", "18", "22:38:17", "EST", "2007" ]
// [ "RunPattern" ]

它顯得更為簡(jiǎn)潔,去除了顯式的 close()。我們可以在這樣一個(gè)單一位置處,使用適宜級(jí)別的測(cè)試或?qū)彶閬?lái)確保沒(méi)有任何問(wèn)題。

1.11 空對(duì)象