返回首页 Groovy 入门

Getting started

设计模式

在 Java 中使用设计模式已成为一种成熟的主题。设计模式也适用于 Groovy:

  • 一些设计模式可以直接沿用(可以利用通常的 Groovy 语法提升特性来实现更好的可读性)。
  • 有些设计模式变得没有用了,因为它们已经是语言内部的组成部分,或者 Groovy 已经找到了更好的办法来达到它们的目的。
  • 对于有些在其他语言中必须在设计层面上表达的模式而言,Groovy 可以直接将其实现(因为 Groovy 可以消弭设计与实现之间的差异)。

1. 模式详述

1.1 抽象工厂模式

抽象工厂模式能将一组有共同主题的独立工厂封装起来。它将普通工厂的意图具体化,例如:不必再利用代码通过使用接口来了解接口背后的具体实施,而应用于一组接口,并且选择一整套能够实现这些接口的具体类。

考虑这样一个范例,其中有接口 Button、TextField 和 Scrollbar。将 WindowsButton、MacButton、FlashButton 作为 Button 的具体类。WindowsScrollBar、MacScrollBar 和 FlashScrollBar 作为 ScrollBar 的具体实现。抽象工厂模式应能让我选择某次想要用到的具体窗口系统(比如 Windows、Mac、Flash),并从此可以编写代码引用这些接口,但能一直在后台使用正确的具体类(来自某个窗口系统)。

1.1.1 范例

假设我们要写一个游戏系统,可能会注意到很多游戏其实都有非常一致的功能与控制方式。

于是我们打算把通用和游戏专有的代码分别写到不同的类中。

首先来看看游戏 Two-Up(一种投掷双币赌博的游戏)专有代码 :

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

下面再来看看猜数字游戏的游戏专有代码:

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

下面再来写工厂代码:

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

工厂的重点在于允许选择整组具体类。

下面是对工厂的使用:

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

第一行配置了所要使用哪一组具体游戏类。其实,通过像第一行中那样使用 factory 属性来确定所用类组并不是很重要。通过其他方式也可以实现这一点。比如,我们可以询问用户想玩的游戏,或者从环境设置中确定使用何种游戏。

根据以上的代码,游戏运行结果大概如下所示:

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,那么范例运行结果如下:

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 适配器模式

适配器模式(有时叫做包装模式)允许对象用在另一类接口。这一模式有两种

1.2.1 委托范例

假设有以下类:

class SquarePeg {
    def width
}

class RoundPeg {
    def radius
}

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

可以询问 RoundHole 类是否有 RoundPeg 适合它,但询问 SquarePeg 相同的问题则不会成功,因为 SquarePeg 根本就没有 radius 属性(比如并不满足必须的接口)。

要想解决这个问题,就需要创建一个适配器使其具有正确的接口,如下所示:

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

也可以像下面这样来使用适配器:

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 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 继承范例

下面使用继承来实现同样的范例。首先,原始类如下(没有任何变动):

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

定义一个闭包作为适配器:

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 来适配

自从 Groovy 1.1 起,就存在一个内建的 MetaClass,能够自动且动态地添加属性和方法。

下例就用到了这个特性:

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

创建完 peg 对象之后,可以立即添加一个属性。不需要改变原始类,也不需要适配类。

1.3 保镖模式

保镖模式(Bouncer Pattern)指的是某个方法唯一目的在于抛出异常(当满足特定条件时)或什么都不做。这种方法经常使用于防卫警戒某方法的前提条件。

当编写工具方法时,应对有可能会出毛病的输入参数保持警惕。在编写内部方法时,应该通过有效的单元测试来保证一直持有特定前提条件。因为在这些情况下,人们可能不会注重方法的防御。

在这一点上,Groovy 区别于其他语言的地方就在于,经常在方法中使用 assert 方法,而不是大量的工具检查器方法或类。

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)
    // 事务逻辑   
}

但用 Groovy 来做,就简单多了:

void doStuff(String name, Object value) {
    assert name != null, 'name should not be null'
    assert value != null, 'value should not be null'
    // 事务逻辑   
}

1.3.2 验证范例

作为一种替代性范例,可能会有下面这样的工具方法:

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 职责链模式

在职责链模式下,有意将使用并实现一个接口(一个或更多的方法)的数个对象松散耦合。一组实现接口的对象按照列表的形式进行组合(或者在极少数情况下按照树的形式)。使用接口的对象向第一个实现器implementor)对象发出请求。第一个实现器对象会确定是否自己执行任何行为,以及是否将该请求沿列表(或树)传播下去。有时,当实现器对象都没有响应请求时,会在模式中编写对一些请求的默认实现。

1.4.1 范例

在该例中,脚本向 lister 对象发送请求,lister 指向 UnixLister 对象。如果它无法处理该请求,则将该请求发送给 WindowsLister。如果还不能处理该请求,则继续发送给 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')

输出一个文件列表(根据操作系统的差别,输出格式将稍有不同)。

下面是 UML 的表示形式:

ChainOfResponsibilityClasses

该模式的变体形式有:

  • 可以利用显式接口(比如 Lister)将实现进行静态类型转换,但由于 duck-typing 特性的存在,这是一种选择方法。

  • 可以使用链树而非列表 if (animal.hasBackbone()) 委托给 VertebrateHandler,否则就委托给 InvertebrateHandler

  • 可以一直沿链传递,即使在处理请求时。

  • 可以在一定的时间不响应不再沿链进行传递。

  • 使用 Groovy 的元编程特性沿链传递未知方法。

1.5 复合模式

复合模式(Composite Pattern)下,一组对象与单个对象的多个实例在处理方式上是相同的。该模式常用于对象层级。通常,对于层级中的叶(leaf)或复合(composite)节点,一个或多个方法的调用方式应该是相同的。在这种情况下,复合节点通常会调用它每一个子节点的同一命名方法。

1.5.1 范例

下面探讨一下复合模式的一个范例,在 LeafComposite 对象上调用 toString()

CompositeClasses

在 Java 中,Component 类是非常重要的,因为它提供了叶节点与复合节点所用的类型。在 Groovy 中,由于 duck-typing,所以不再需要用它来完成这种用途,但该类依然非常有用,可以在叶节点与复合节点间放置常见的行为。

根据目标,我们将构建以下的层级:

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)

结果输出如下:

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

1.6 装饰器模式

装饰器模式(Decorator Pattern)可以在不改变一个对象的基本接口的情况装饰其行为。当需要原始对象(未被装饰对象)时,被装饰对象可以被替换。装饰行为通常并不会包含对原始对象源码的修改,装饰器应该能以各种灵活的手段进行组合,从而形成带有多种装饰行为的对象。

1.6.1 典型范例

假设现在有一个 Logger 类:

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

有时为日志记录添加时间戳是非常有用的,或者可能想改变消息的大小写格式。我们可能会试图将所有的功能都塞进 Logger 类中。这会使得 Logger 类变得十分复杂臃肿。另外,每一个人都将获得所有的功能,即使你不想要其中的某个小子集,也毫无办法。最后,功能交互就会变得极难控制。

为了克服这些缺点,定义两个装饰器类。Logger 类的用途在于可自由地装饰它们的基本记录器,其中可按照任何预想顺序用到多个(或不用)装饰器类。这样的类如下所示:

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

由上可见,我们用了两个装饰器来装饰 logger 的行为。根据我们选用的装饰器使用顺序,日志消息最终结果是全部大写字母,而时间戳则还是正常的格式。如果我们交换顺序,则结果如下:

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

注意时间戳本身也被改成了大写。

1.6.2 探讨一下动态行为

之前的装饰器都是 Logger 类所特有的。我们可以利用 Groovy 的元编程特性创建一个装饰器,使其自然具有更通用的功用,比如像下面这个类:

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

它将接受任何类并对其进行装饰,从而使任何 String 方法的参数都能自动变更为小写。

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

只是要注意这里的顺序。原始的装饰器被局限于只能装饰 Logger 对象。这个装饰器适用于任何对象类型,所以不能将顺序调换,比如不能像下面这样:

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

在运行时生成一个正确的 Proxy 类型,就可以克服这个限制,但在这个例子中,我们不想搞得那么复杂。

1.6.3 运行时行为装饰

你现在可能还在认为,像从 Groovy 1.1 开始一直所采用的那样,利用ExpandoMetaClass 来动态装饰类的行为。这虽然不是装饰器模式通常的风格(当然谈不上有多灵活),但却可能在有些场合帮你实现相似的结果,而无需创建新类。

下面是范例代码:

// ExpandoMetaClass 的当前使用机制
GroovySystem.metaClassRegistry.metaClassCreationHandle = new ExpandoMetaClassCreationHandle()

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

这样做虽然也和应用了一个单独的装饰器的效果相似,但却无法轻松地立刻使用并去除装饰。

1.6.4 更多动态装饰

假设有一个计算器类(实际上任何类都可以当做范例)。

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

我们可能会需要观察类在一段时间的使用情况。如果它深埋在代码基中,可能很难判断它是何时调用的以及所用的参数。另外,也很难知道它是否成功执行。我们可以轻松地创建一个通用的跟踪装饰器,只要 Calc 类上的任何方法一被调用,就让它打印出跟踪信息,以及提供方法执行的时间信息。下面就是这个跟踪装饰器的代码:

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

下面是如何在脚本中使用这个类:

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

运行该脚本,所得结果如下:

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

1.6.5 利用拦截器进行装饰

上面的计时范例与 Groovy 对象的生命周期(通过 invokeMethod)。这是一种实现元编程的非常重要的方式,以至于 Groovy 为这一使用拦截器interceptors)的装饰方式提供了特殊支持。

Groovy 甚至还内建了 TracingInterceptor。还可以像下面这样扩展内建类:

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

有关于这个新类,使用范例如下所示:

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

输出结果如下:

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 进行装饰

如果想装饰一个对象(比如只是某个类的一个特殊实例,而不是类本身),可以使用 Java 的 java.lang.reflect.Proxy。Groovy 这样做起来要更简单一些。下面这个代码范例取自一个 grails 项目,它封装了 java.sql.Connection,所以它的 close 方法是无参的。

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

如果有很多方法需要拦截,那么经过修改后,该方法可以按照方法名查找映射中的闭包,并加以调用。

1.6.7 利用 Spring 进行装饰

Spring 框架允许利用拦截器来应用装饰器(你可能听过名词 advice 或 aspect)。也可以用 Groovy 来实现这种机制。

首先定义一个需要装饰的类(另外也会用到一个接口,因为这是 Spring 的惯例)。

假设该接口如下:

interface Calc {
    def add(a, b)
}

类如下:

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

下面,在一个 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)

运行脚本的结果如下:

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

为了显示在日志级别 FINEST 的消息,我们必须调整 logging.properties 文件。

1.6.8 使用 GPars 的异步装饰者

Panini 上的代码范例有一定的启发性。避免使用 @AddedBehavior 标记。

@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)对象行为(公共方法)由委托的一个或多个对象实现。

Groovy 还允许使用委托模式的传统方式,比如用委托代替继承

1.7.1 利用 ExpandoMetaClass 实现委托模式

groovy.lang.ExpandoMetaClass 允许通过库封装来使用该模式。这能让 Groovy 效仿 Ruby 语言所用的类似库。

考虑下面这个库类:

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

将它放在类路径中,可以像上例所示的那样动态地应用委托模式。首先假设我们有以下这些类:

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 自动地从 lender 对象处借调方法来扩展 Person 类。我们可以照原样借调方法,也可以对方法改名。

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

def p = new Person()

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

上述代码中的第一行中,通过委托给 lender 对象,为 Person 类添加了 borrowFor 方法。第二行中,通过委托给 lender 对象的 borrowAmount 方法,为 Person 类添加了一个 getMoney 方法。

另外,我们还可以借调多个方法:

delegator.delegateAll 'borrowFor', 'borrowAmount'

这会将两种方法都添加到 Person 类上。

或者,有时我们会需要所有方法:

delegator.delegateAll()  

从而使 Person 类能够使用委托对象中的所有方法。

另外,可以利用映射标记重新命名多个方法:

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

1.7.2 利用 @Delegate 标记实现委托模式

从 1.6 版开始,可以使用内建的基于 AST 变换的委托模式。

委托可以变得更简单:

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)能够极大减少内存需求,因为它可以避免在处理包含很多极其相似部分的系统时,大量地去创建重量级对象。例如,利用一个复杂的字符类来构建文档,该字符类要处理unicode、字体以及定位等情况。文档越大,如果文档中的每一个实际字符都需要一个特有的字符类实例的话,内存需求显然就会越大。相反,由于字符本身可能保存在字符串中,我们可能仅仅会用到单个的了解字符处理规范的字符类(或少量的字符类,比如一个字符类对应一个字体类型)。

在这种情境下,我们把这种由多个事物(比如字符类型)所共享的状态定义为内部instrinsic)状态。它被重量级类所捕获。区分实际字符(可能只是它的 ASCII 码或 Unicode)的状态被称为外部extrinsic)状态。

1.8.1 范例

首先我们构建一些较为复杂的飞机(第一种飞机与第二种存在一定的竞争关系,但有这一点与范例毫无关系):

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

b797-hoax

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

a380

如果想打造自己的机队,我们首先可能会去使用这些重量级类的很多实例。但在实际情况下,各架飞机可能只有很小的状态(外部状态)改动,所以我们还是借助重量级对象的单例,分别捕获外部状态即可(购买日期与资产编号,具体可参见下面的代码):

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

class Aircraft {
    private type         // 内部状态   
    private assetNumber  // 外部状态   
    private bought       // 外部状态   
    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() }

所以,按照这种方法,假如机队有几百家飞机的话,我们完全可以让一个重量级对象来对应一种类型的飞机。

但为了更进一步的高效率诉求,我们不再预先创建初始映射,而可能只延迟使用享元对象。

上面脚本运行结果为:

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 迭代器模式

迭代器模式不暴露底层表达的情况下,连续访问聚合对象的元素。

Groovy 在很多闭包操作符中内建有迭代器模式,比如 eacheachWithIndex,以及 for .. in 循环。

例如:

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

输出结果如下:

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]

另一个范例:

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

结果为:

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

另外,在其他一些专有操作符中也内建了迭代器模式,这些操作符包括:eachByteeachFileeachDireachLineeachObjecteachMatch,以便用于处理流、URL、文件、目录以及正则表达式的匹配。

1.10 贷出资源模式

贷出资源模式(Loan my Resource) 用于确保资源一旦超出范围后,就能得到处置。

许多 Groovy 的辅助方法都内建有这种模式。如果需要处理 Groovy 支持范围之外的资源,就应该考虑使用这一模式。

1.10.1 范例

下面这个范例代码用于处理文件。首先写入一些代码,然后打印该文件的尺寸:

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

下面读取文件内容,每次读取并打印一行内容。

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

注意,Groovy 其实在幕后使用了Java 中常见的 ReaderPrintWriter 对象,开发者不用担心如何显式地创建或关闭这些资源。内建的 Groovy 方法向闭包代码贷出相应的 reader 或 writer 对象,然后再进行自我整理,因而无需进行任何额外的操作。

但是,相对于使用 Groovy 内建机制所达成的结果,你可能更希望实现一些稍微不同的结果。所以你应该考虑在你自己的资源处理操作中使用该模式。

下面来考虑如何处理文件中每行中的单词列表。可以使用 Groovy 内建的函数来实现,但请忍耐一下,假设我们可以自己来处理一些资源。在不使用该模式的情况下进行编码:

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

注意在代码中我们显式地调用了 close()。假如我们没有正确地编码(这里没有用 try …​ finally 代码块包围代码),就会有将文件处理公开的危险性。

下面使用贷出模式,首先编写一个辅助方法:

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

然后重写代码,如下所示:

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

它显得更为简洁,去除了显式的 close()。我们可以在这样一个单一位置处,使用适宜级别的测试或审查来确保没有任何问题。

1.11 空对象模式

空对象模式(Null Object Pattern)使用了特殊的放置标记对象来表示空对象。一般来说,如果有一个空引用,则不能调用 reference.fieldreference.method(),会收到可怕的 NullPointerException 异常。空对象模式使用特殊的对象来表示空对象,以避免使用实际的 null,从而可以调用空对象上的字段和方法引用。空对象的使用结果在字面上应等同于“什么都不做”。

1.11.1 简单范例

假设有一个这样的系统:

class Job {
    def salary
}

class Person {
    def name
    def Job job
}

def people = [
    new Person(name: 'Tom', job: new Job(salary: 1000)),
    new Person(name: 'Dick', job: new Job(salary: 1200)),
]

def biggestSalary = people.collect { p -> p.job.salary }.max()
println biggestSalary

运行后,打印结果为 1200,假设现在像下面这样调用:

people << new Person(name: 'Harry')

如果再次计算 biggestSalary 就会得到空指针异常。

为了解决这个问题,引入一个 NullJob 类,改变上面语句为:

class NullJob extends Job { def salary = 0 }

people << new Person(name: 'Harry', job: new NullJob())
biggestSalary = people.collect { p -> p.job.salary }.max()
println biggestSalary

这实现了我们的期望目标,但却不是 Groovy 的最佳方式。利用 Groovy 的安全解除引用操作符(?.)和对空值敏感的一些闭包,往往不必去创建特殊的空对象或空类。下面就是对上述范例采取更为 Groovy 方式的做法:

people << new Person(name:'Harry')
biggestSalary = people.collect { p -> p.job?.salary }.max()
println biggestSalary

这里要注意两件事。首先,max() 对空值敏感,所以 [300, null, 400].max() == 400.。其次,利用 ?. 操作符,对于p?.job?.salary 这样的表达式,当 salaryjobp 为空时,表达式则为空。为了避免出现 NullPointerException 异常,不必编写复杂、层层内嵌的 if...then...else 语句。

1.11.2 树范例

下面范例展示了如何在树形结构中,计算所有数值的大小、累计加和及累计乘积。

首先在计算方法中引入处理空值的逻辑。

class NullHandlingTree {
    def left, right, value

    def size() {
        1 + (left ? left.size() : 0) + (right ? right.size() : 0)
    }

    def sum() {
       value + (left ? left.sum() : 0) + (right ? right.sum() : 0)
    }

    def product() {
       value * (left ? left.product() : 1) * (right ? right.product() : 1)
    }
}

def root = new NullHandlingTree(
    value: 2,
    left: new NullHandlingTree(
        value: 3,
        right: new NullHandlingTree(value: 4),
        left: new NullHandlingTree(value: 5)
    )
)

println root.size()
println root.sum()
println root.product()  

如果引入空对象模式(定义 NullTree 对象),那么就可以简化 size()sum() 以及 product() 方法中的逻辑,这些方法从而就能更为清晰地展现通常情况下的逻辑。

class Tree {
    def left = new NullTree(), right = new NullTree(), value

    def size() {
        1 + left.size() + right.size()
    }

    def sum() {
       value + left.sum() + right.sum()
    }

    def product() {
       value * left.product() * right.product()
    }
}

class NullTree {
    def size() { 0 }
    def sum() { 0 }
    def product() { 1 }
}

def root = new Tree(
    value: 2,
    left: new Tree(
        value: 3,
        right: new Tree(value: 4),
        left: new Tree(value: 5)
    )
)

println root.size()
println root.sum()
println root.product()

范例的运行结果是:

4
14
120  

注意:空对象模式有个特殊变体,那就是将它与单例模式组合使用的情况。因此,在需要像上例中的那个空对象时,我们不能编写新的 NullTree(),而是应该用一个必要时放在数据结构中的空对象实例。

1.12 操控库模式

当我们需要更进一步增强库的功能时,操控库模式(Pimp my Library Pattern)提供了一种扩展库的方法。它的假定条件是我们并没有库的源码。

1.12.1 范例

假设现在想使用 Groovy 中内建的整数工具(建立于 Java 中已存在的功能)。这些库已经拥有我们需要的大部分功能。由于可能没有所有的库源码,所以不能只改变库,而要扩充库。Groovy 可以采用多种方法来实现这一点,其中一个方法就是使用类别。

首先定义一个适合的类别。

class EnhancedInteger {
    static boolean greaterThanAll(Integer self, Object[] others) {
        greaterThanAll(self, others)
    }
    static boolean greaterThanAll(Integer self, others) {
        others.every { self > it }
    }
}

添加了两个方法,它们通过提供 greaterThanAll 方法扩充了整数方法。类别遵守着一定的规范,它们被定义为静态方法,并利用特殊的第一个参数来代表我们希望扩展的类。静态方法 greaterThanAll(Integer self, others) 成为 greaterThanAll(other) 的实例方法。

现在已经定义了两种 greaterThanAll。其中一个适用于集合、范围等类型,另一个则适用于一些 Intege 参数。

该类别的使用方法如下:

use(EnhancedInteger) {
    assert 4.greaterThanAll(1, 2, 3)
    assert !5.greaterThanAll(2, 4, 6)
    assert 5.greaterThanAll(-4..4)
    assert 5.greaterThanAll([])
    assert !5.greaterThanAll([4, 5])
}

如你所见,通过这种技术,在无需访问源码的情况下,我们可以高效地丰富原始类。更进一步地是,我们不但可以为系统的不同部分应用不同的改进,而且还可以在需要时利用未增强的对象。

1.13 代理模式

代理模式(Proxy Pattern)允许利用一种对象来伪装并替代另一些对象。通常在使用中,代理对象并不会被觉察出来。该模式非常适用于那些真实对象难以创建并使用的环境,比如当网络连接、内存中的巨型对象,或者文件、数据库以及其他一些很难或基本无法复制的资源。

1.13.1 范例

该模式常见的应用环境是涉及到一个不同 JVM 的远程对象。下面的范例是网络连接中的客户端代码,它创建的代理通过 Socket 连接与服务器对象进行通信:

class AccumulatorProxy {
    def accumulate(args) {
        def result
        def s = new Socket("localhost", 54321)
        s.withObjectStreams { ois, oos ->
            oos << args
            result = ois.readObject()
        }
        s.close()
        return result
    }
}

println new AccumulatorProxy().accumulate([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
// => 55

而服务器代码可能如下所示(首先启动它):

class Accumulator {
    def accumulate(args) {
        args.inject(0) { total, arg -> total += arg }
    }
}

def port = 54321
def accumulator = new Accumulator()
def server = new ServerSocket(port)
println "Starting server on port $port"
while(true) {
    server.accept() { socket ->
        socket.withObjectStreams { ois, oos ->
            def args = ois.readObject()
            oos << accumulator.accumulate(args)
        }
    }
}

1.14 单例模式

单例模式(Singleton Pattern)可以确保一个具体的类只创建过一个对象。它非常适用于系统中只需要一个对象来协调行为:一方面是处于在有些情况下,创建多个等同的

单例模式的缺点包括以下两方面:

  • 减少重用。比如,如果想利用单例模式来继承,就会出现问题。如果 SingletonB 扩展了 SingletonA,那么就会(最多)只有一个实例。如果你想让两个类都有一个实例,那么如何重写 getInstance() 静态方法呢?

  • 由于静态方法,通常也很难测试单例,但如果需要时,Groovy 也能支持这一点。

1.14.1 经典 Java 单例

假设要设计一个收集选票的类,由于选票数量是非常重要的因素,所以决定使用单例模式。VoteCollector 对象只能出现一个,所以很容易创建并使用对象。

class VoteCollector {
    def votes = 0
    private static final INSTANCE = new VoteCollector()
    static getInstance() { return INSTANCE }
    private VoteCollector() { }
    def display() { println "Collector:${hashCode()}, Votes:$votes" }
}

代码中值得注意的是:

  • 包含一个私有的构造函数,因而不可能在系统中创建 VoteCollector 对象(除了我们创建的 INSTANCE)。
  • INSTANCE 也是私有的,所以一旦设置好了,就无法更改它。
  • 暂时,选票更新还无法做到线程安全(该范例没有提供这个功能)。
  • 选票收集器实例的创建并不是延后创建的(如果我们永远不引用该类,就不会创建该实例;然而,一旦引用该类,就会创建该实例,即使一开始并不需要)。

可以在一些脚本代码中使用该单例类:

def collector = VoteCollector.instance
collector.display()
collector.votes++
collector = null

Thread.start{
    def collector2 = VoteCollector.instance
    collector2.display()
    collector2.votes++
    collector2 = null
}.join()

def collector3 = VoteCollector.instance
collector3.display()

实例被使用了 3 次,第 2 次使用甚至是处于不同的线程(但不要在存在一个新的类加载器的情况下这样做)。

运行该脚本会得到以下输出(hashcode 值会变更):

Collector:15959960, Votes:0
Collector:15959960, Votes:1
Collector:15959960, Votes:2

该模式的变体形式为:

  • 为了支持延后加载及多线程,可以只使用 synchronized 关键字和 getInstance() 方法。虽然在性能上可能会出现问题,但却奏效。
  • 包含双重检查锁定模式和 volatile 关键字(对于 Java 5 及以前版本),但是这种做法存在以下局限性:局限性

1.14.2 范例:通过元编程实现单例模式

利用 Groovy 元编程功能可以让类似单例模式这样的概念得到更为根本的展现。下面的范例展示了如何使用 Groovy 的元编程功能实现单例模式,但不一定是最高效的方式。

假设要跟踪计算器执行的计算总数,一种方法是为计算器类使用单例,在类中设置一个保存计数的变量。

首先定义一些基础类。Calculator 类用于执行计算,并记录执行了多少次计算。Client 类是计算器的外在接口。

class Calculator {
    private total = 0
    def add(a, b) { total++; a + b }
    def getTotalCalculations() { 'Total Calculations: ' + total }
    String toString() { 'Calc: ' + hashCode() }
}

class Client {
    def calc = new Calculator()
    def executeCalc(a, b) { calc.add(a, b) }
    String toString() { 'Client: ' + hashCode() }
}

接着,定义并注册一个超级类MetaClass),阻止再创建 Calculator 对象,并提供一个预先创建好的实例。也要在 Groovy 系统中注册这个超级类。

class CalculatorMetaClass extends MetaClassImpl {
    private static final INSTANCE = new Calculator()
    CalculatorMetaClass() { super(Calculator) }
    def invokeConstructor(Object[] arguments) { return INSTANCE }
}

def registry = GroovySystem.metaClassRegistry
registry.setMetaClass(Calculator, new CalculatorMetaClass())

现在,就可以从脚本中使用 Client 类的实例。Client 类试图创建新的计算器实例,但却得到的是单例。

def client = new Client()
assert 3 == client.executeCalc(1, 2)
println "$client, $client.calc, $client.calc.totalCalculations"

client = new Client()
assert 4 == client.executeCalc(2, 2)
println "$client, $client.calc, $client.calc.totalCalculations"

运行该脚本的结果如下(hashcode 值可能不同):

Client: 7306473, Calc: 24230857, Total Calculations: 1
Client: 31436753, Calc: 24230857, Total Calculations: 2  

1.14.3 Guice 范例

也可以使用 Guice 来实现单例模式。

还是以计算器为例。

Guice 是一种面向 Java 的框架,支持面向接口的设计,因而首先创建 Calculator 接口。然后创建 CalculatorImpl 实现,以及一个用来与脚本交互的 Client 对象。严格意义上,本例并不需要Client 类,但它却能让我们展示非单例实例是默认的。代码如下:

@Grapes([@Grab('aopalliance:aopalliance:1.0'), @Grab('com.google.code.guice:guice:1.0')])
import com.google.inject.*

interface Calculator {
    def add(a, b)
}

class CalculatorImpl implements Calculator {
    private total = 0
    def add(a, b) { total++; a + b }
    def getTotalCalculations() { 'Total Calculations: ' + total }
    String toString() { 'Calc: ' + hashCode() }
}

class Client {
    @Inject Calculator calc
    def executeCalc(a, b) { calc.add(a, b) }
    String toString() { 'Client: ' + hashCode() }
}

def injector = Guice.createInjector (
    [configure: { binding ->
        binding.bind(Calculator)
               .to(CalculatorImpl)
               .asEagerSingleton() } ] as Module
)

def client = injector.getInstance(Client)
assert 3 == client.executeCalc(1, 2)
println "$client, $client.calc, $client.calc.totalCalculations"

client = injector.getInstance(Client)
assert 4 == client.executeCalc(2, 2)
println "$client, $client.calc, $client.calc.totalCalculations"

注意 Client 类中的 @Inject 注释。可以在代码中正确地指明注入的字段。

在该例中,我们选择使用显式的绑定。所有的依赖关系(目前只有一个)都配置在绑定中。Guice 注入器了解绑定并在创建对象时注入所需的依赖。为了维持单例模式,必须使用 Guice 来创建实例。至今为止,你还可以手动地使用 CalculatorImpl() 创建计算器实例,这显然会侵犯预定的单例行为。

在其他的一些情况下(可能在大型系统中),我们可以用注释来表达依赖,如下所示:

@Grapes([@Grab('aopalliance:aopalliance:1.0'), @Grab('com.google.code.guice:guice:1.0')])
import com.google.inject.*

@ImplementedBy(CalculatorImpl)
interface Calculator {
    // as before ...
}

@Singleton
class CalculatorImpl implements Calculator {
    // as before ...
}

class Client {
    // as before ...
}

def injector = Guice.createInjector()

// ...

注意 CalculatorImpl 类中的 @Singleton 注释,以及 Calculator 接口中的 @ImplementedBy 注释。

使用任何一种方法运行上述范例后的结果为(hashcode 值可能会不同):

Client: 8897128, Calc: 17431955, Total Calculations: 1  
Client: 21145613, Calc: 17431955, Total Calculations: 2  

无论在什么时候,只要请求实例,就会获得了一个新的客户端对象,但它却被同样一个计算器对象所注入。

1.14.4 Spring 范例

使用 Spring 再来实现一下计算器范例:

@Grapes([@Grab('org.springframework:spring-core:3.2.2.RELEASE'), @Grab('org.springframework:spring-beans:3.2.2.RELEASE')])
import org.springframework.beans.factory.support.*

interface Calculator {
    def add(a, b)
}

class CalculatorImpl implements Calculator {
    private total = 0
    def add(a, b) { total++; a + b }
    def getTotalCalculations() { 'Total Calculations: ' + total }
    String toString() { 'Calc: ' + hashCode() }
}

class Client {
    Client(Calculator calc) { this.calc = calc }
    def calc
    def executeCalc(a, b) { calc.add(a, b) }
    String toString() { 'Client: ' + hashCode() }
}

// 通过 API 来连接依赖。
// 还可以使用基于 XML 的配置,或 Grails Bean Builder DSL。   
def factory = new DefaultListableBeanFactory()
factory.registerBeanDefinition('calc', new RootBeanDefinition(CalculatorImpl))
def beanDef = new RootBeanDefinition(Client, false)
beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_AUTODETECT)
factory.registerBeanDefinition('client', beanDef)

def client = factory.getBean('client')
assert 3 == client.executeCalc(1, 2)
println "$client, $client.calc, $client.calc.totalCalculations"

client = factory.getBean('client')
assert 4 == client.executeCalc(2, 2)
println "$client, $client.calc, $client.calc.totalCalculations"  

运行结果如下(hashcode 值可能不同):

Client: 29418586, Calc: 10580099, Total Calculations: 1
Client: 14800362, Calc: 10580099, Total Calculations: 2    

1.14.5 更多资源

1.15 状态模式

状态模式(State Pattern)采用一种结构化的方法来分割复杂系统中的行为。系统的整体行为会被分成各种定义良好的状态。通常,每个状态由一个独立的类来实现。首先,通过了解当前状态来确定整体系统行为;然后,在该状态中了解可能的行为(体现在对应此状态的类的方法中)。

1.15.1 范例

范例如下:

class Client {
    def context = new Context()
    def connect() {
        context.state.connect()
    }
    def disconnect() {
        context.state.disconnect()
    }
    def send_message(message) {
        context.state.send_message(message)
    }
    def receive_message() {
        context.state.receive_message()
    }
}

class Context {
    def state = new Offline(this)
}

class ClientState {
    def context
    ClientState(context) {
        this.context = context
        inform()
    }
}

class Offline extends ClientState {
    Offline(context) {
        super(context)
    }
    def inform() {
        println "offline"
    }
    def connect() {
        context.state = new Online(context)
    }
    def disconnect() {
        println "error: not connected"
    }
    def send_message(message) {
        println "error: not connected"
    }
    def receive_message() {
        println "error: not connected"
    }
}

class Online extends ClientState {
    Online(context) {
        super(context)
    }
    def inform() {
        println "connected"
    }
    def connect() {
        println "error: already connected"
    }
    def disconnect() {
        context.state = new Offline(context)
    }
    def send_message(message) {
        println "\"$message\" sent"
    }
    def receive_message() {
        println "message received"
    }
}

client = new Client()
client.send_message("Hello")
client.connect()
client.send_message("Hello")
client.connect()
client.receive_message()
client.disconnect()

输出结果如下:

offline
error: not connected
connected
"Hello" sent
error: already connected
message received
offline

Groovy 这样的动态语言中有一个很棒的功能,那就是可以根据我们的需求,利用多种方式来表达该范例。下面来介绍该范例的各种变体形式。

1.15.2 变体1:利用面向接口的设计

我们可以采取利用面向接口的设计的办法,为此引入下面的接口:

interface State {
    def connect()
    def disconnect()
    def send_message(message)
    def receive_message()
}

然后就可以修改 ClientOnlineOffline 类来实现该接口,比如:

class Client implements State {
  // ... as before ...
}

class Online implements State {
  // ... as before ...
}

class Offline implements State {
  // ... as before ...
}

你可能会想:我们不是刚介绍过额外的样板文件代码吗?难道不能依赖 duck-typing 吗?我们可以侥幸使用 duck-typing,但状态模式的一个关键目的在于分割复杂性。如果我们知道 client 类与每一个 state 类都满足一个接口,那么我们就能对复杂性设置一些关键限制。我们就可以独立地查看任何状态类,并且知道与该状态有关的可能行为限制。

其实不必非得使用接口来完成,但它确实能助于表达这种特殊分割方式的意图,并且有助于减少单元测试的尺寸(对于面向接口设计支持得较弱的语言,则必须添加额外的测试来表达这种意图)。

1.15.3 变体2:抽取状态模式逻辑

作为替代(或者与其他变体相组合),我们可能抽取一部分状态模式逻辑,将其应用到辅助类中。如下例所示,可以在状态模式包/jar/脚本中定义这些类:

abstract class InstanceProvider {
    static def registry = GroovySystem.metaClassRegistry
    static def create(objectClass, param) {
        registry.getMetaClass(objectClass).invokeConstructor([param] as Object[])
    }
}

abstract class Context {
    private context
    protected setContext(context) {
        this.context = context
    }
    def invokeMethod(String name, Object arg) {
        context.invokeMethod(name, arg)
    }
    def startFrom(initialState) {
        setContext(InstanceProvider.create(initialState, this))
    }
}

abstract class State {
    private client

    State(client) { this.client = client }

    def transitionTo(nextState) {
        client.setContext(InstanceProvider.create(nextState, client))
    }
}   

它们都非常具有通用性,可以应用于任何引入状态模式的情况。下面是相应的代码:

class Client extends Context {
    Client() {
        startFrom(Offline)
    }
}

class Offline extends State {
    Offline(client) {
        super(client)
        println "offline"
    }
    def connect() {
        transitionTo(Online)
    }
    def disconnect() {
        println "error: not connected"
    }
    def send_message(message) {
        println "error: not connected"
    }
    def receive_message() {
        println "error: not connected"
    }
}

class Online extends State {
    Online(client) {
        super(client)
        println "connected"
    }
    def connect() {
        println "error: already connected"
    }
    def disconnect() {
        transitionTo(Offline)
    }
    def send_message(message) {
        println "\"$message\" sent"
    }
    def receive_message() {
        println "message received"
    }
}

client = new Client()
client.send_message("Hello")
client.connect()
client.send_message("Hello")
client.connect()
client.receive_message()
client.disconnect()

如上所示,有了 startFromtransitionTo 方法,范例开始有了点 DSL 的感觉。

1.15.4 变体3:DSL

作为替代(或者与其他变体相组合),我们还可以全面使用领域特定语言(DSL)来处理该例。

定义下面这些通用辅助函数(首先在此讨论):

class Grammar {
    def fsm

    def event
    def fromState
    def toState

    Grammar(a_fsm) {
        fsm = a_fsm
    }

    def on(a_event) {
        event = a_event
        this
    }

    def on(a_event, a_transitioner) {
        on(a_event)
        a_transitioner.delegate = this
        a_transitioner.call()
        this
    }

    def from(a_fromState) {
        fromState = a_fromState
        this
    }

    def to(a_toState) {
        assert a_toState, "Invalid toState: $a_toState"
        toState = a_toState
        fsm.registerTransition(this)
        this
    }

    def isValid() {
        event && fromState && toState
    }

    public String toString() {
        "$event: $fromState=>$toState"
    }
}

class FiniteStateMachine {
    def transitions = [:]

    def initialState
    def currentState

    FiniteStateMachine(a_initialState) {
        assert a_initialState, "You need to provide an initial state"
        initialState = a_initialState
        currentState = a_initialState
    }

    def record() {
        Grammar.newInstance(this)
    }

    def reset() {
        currentState = initialState
    }

    def isState(a_state) {
        currentState == a_state
    }

    def registerTransition(a_grammar) {
        assert a_grammar.isValid(), "Invalid transition ($a_grammar)"
        def transition
        def event = a_grammar.event
        def fromState = a_grammar.fromState
        def toState = a_grammar.toState

        if (!transitions[event]) {
            transitions[event] = [:]
        }

        transition = transitions[event]
        assert !transition[fromState], "Duplicate fromState $fromState for transition $a_grammar"
        transition[fromState] = toState
    }

    def fire(a_event) {
        assert currentState, "Invalid current state '$currentState': passed into constructor"
        assert transitions.containsKey(a_event), "Invalid event '$a_event', should be one of ${transitions.keySet()}"
        def transition = transitions[a_event]
        def nextState = transition[currentState]
        assert nextState, "There is no transition from '$currentState' to any other state"
        currentState = nextState
        currentState
    }
}

现在开始定义并测试状态机:

class StatePatternDslTest extends GroovyTestCase {
    private fsm

    protected void setUp() {
        fsm = FiniteStateMachine.newInstance('offline')
        def recorder = fsm.record()
        recorder.on('connect').from('offline').to('online')
        recorder.on('disconnect').from('online').to('offline')
        recorder.on('send_message').from('online').to('online')
        recorder.on('receive_message').from('online').to('online')
    }

    void testInitialState() {
        assert fsm.isState('offline')
    }

    void testOfflineState() {
        shouldFail{
            fsm.fire('send_message')
        }
        shouldFail{
            fsm.fire('receive_message')
        }
        shouldFail{
            fsm.fire('disconnect')
        }
        assert 'online' == fsm.fire('connect')
    }

    void testOnlineState() {
        fsm.fire('connect')
        fsm.fire('send_message')
        fsm.fire('receive_message')
        shouldFail{
            fsm.fire('connect')
        }
        assert 'offline' == fsm.fire('disconnect')
    }
}

该范例与其他范例并不完全等同,没有用到预先定义的 OnlineOffline 类,而是在需要时定义了整个状态机。关于这一方式更多精心制作的范例,可以参看这里

也可以参考:使用 ModelJUnit 的基于模型的测试

1.16 策略模式

策略模式(Strategy Pattern)可以在使用过程中抽象出特定算法,允许切换算法而根本不需要改变调用指令。该模式的一般形式为:

StrategyClasses

在 Groovy 中,由于把代码看做使用匿名方法(宽泛地叫做闭包)的第一类对象,策略模式的用武之地变少了。可以直接把算法放入闭包中即可。

1.16.1 范例

封装策略模式的典型用法如下所示:

interface Calc {
    def execute(n, m)
}

class CalcByMult implements Calc {
    def execute(n, m) { n * m }
}

class CalcByManyAdds implements Calc {
    def execute(n, m) {
        def result = 0
        n.times{
            result += m
        }

        result
    }
}

def sampleData = [
    [3, 4, 12],
    [5, -5, -25]
]

Calc[] multiplicationStrategies = [
    new CalcByMult(),
    new CalcByManyAdds()
]

sampleData.each{ data ->
    multiplicationStrategies.each { calc ->
        assert data[2] == calc.execute(data[0], data[1])
    }
}

定义了一个接口 Calc,它将由具体的策略类来实现(也可以使用一个抽象类)。然后,为了简单的乘法运算,我们定义了两种算法:CalcByMult 是普通方式,CalcByManyAdds 只使用加法(不要使用负数,虽然可以解决这个问题,但那样范例代码就太长了)。使用标准的 polymorphism 来调用这些算法。

更为 Groovy 的方式是使用闭包:

def multiplicationStrategies = [
    { n, m -> n * m },
    { n, m -> def result = 0; n.times{ result += m }; result }
]

def sampleData = [
    [3, 4, 12],
    [5, -5, -25]
]

sampleData.each{ data ->
    multiplicationStrategies.each { calc ->
        assert data[2] == calc(data[0], data[1])
    }
}

1.17 模板方法模式

模板方法模式 抽象出一些算法的细节。算法的通用部分都包含在一个基本类中。特定实现细节也就位于这些基本类中。所包括的类的常见模式如下所示:

TemplateMethodClasses

1.17.1 范例

在该范例中,Accumulator 会获取累计算法的核心细节信息。基本类 SumProduct 用来提供特定的自定义方式,以便使用通用的累计算法。

abstract class Accumulator {
    protected initial
    abstract doAccumulate(total, v)
    def accumulate(values) {
        def total = initial
        values.each { v -> total = doAccumulate(total, v) }
        total
    }
}

class Sum extends Accumulator {
    def Sum() { initial = 0 }
    def doAccumulate(total, v) { total + v }
}

class Product extends Accumulator {
    def Product() { initial = 1 }
    def doAccumulate(total, v) { total * v }
}

println new Sum().accumulate([1,2,3,4])
println new Product().accumulate([1,2,3,4])

结果输出为:

10
24

在这种特殊的情况下,可以使用 Groovy 的注入方法来实现相似的结果(使用闭包):

Closure addAll = { total, item -> total += item }
def accumulated = [1, 2, 3, 4].inject(0, addAll)
println accumulated    // => 10

感谢 duck-typing,同样也适用于支持 add 方法(Groovy 中是 plus())的对象。比如:

在这种特殊的情况下,可以使用 Groovy 的注入方法来实现相似的结果(使用闭包):

accumulated = [ "1", "2", "3", "4" ].inject("", addAll)
println accumulated    // => "1234"

也可以像下面这样来处理乘法:

Closure multAll = { total, item -> total *= item }
accumulated = [1, 2, 3, 4].inject(1, multAll)
println accumulated    // => 24

像这样来使用闭包看起来更像是策略模式,但如果我们意识到内建的注入方法是模板方法算法的通用部分,闭包就会成为模板方法模式的自定义部分。

1.18 访问者模式

访问者模式(Visitor Pattern)是一种虽然知名但却并不经常用到的模式。我认为这很奇怪,因为它实在是一种非常出色的模式。

该模式的目标就是将算法与对象结构中分离出来。通过这种分离,可以为已有的对象结构添加新的操作,而不更改这些结构。

1.18.1 简单范例

该范例处理的是如何计算图形(或图形集合)边界的问题。我们首先尝试利用传统的访问者模式来实现,然后再用更为 Groovy 风格的方式来实现。

abstract class Shape { }

class Rectangle extends Shape {
    def x, y, width, height

    Rectangle(x, y, width, height) {
        this.x = x; this.y = y; this.width = width; this.height = height
    }

    def union(rect) {
        if (!rect) return this
        def minx = [rect.x, x].min()
        def maxx = [rect.x + width, x + width].max()
        def miny = [rect.y, y].min()
        def maxy = [rect.y + height, y + height].max()
        new Rectangle(minx, miny, maxx - minx, maxy - miny)
    }

    def accept(visitor) {
        visitor.visit_rectangle(this)
    }
}

class Line extends Shape {
    def x1, y1, x2, y2

    Line(x1, y1, x2, y2) {
        this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2
    }

    def accept(visitor){
        visitor.visit_line(this)
    }
}

class Group extends Shape {
    def shapes = []
    def add(shape) { shapes += shape }
    def remove(shape) { shapes -= shape }
    def accept(visitor) {
        visitor.visit_group(this)
    }
}

class BoundingRectangleVisitor {
    def bounds

    def visit_rectangle(rectangle) {
        if (bounds)
            bounds = bounds.union(rectangle)
        else
            bounds = rectangle
    }

    def visit_line(line) {
        def line_bounds = new Rectangle(line.x1, line.y1, line.x2-line.y1, line.x2-line.y2)
        if (bounds)
            bounds = bounds.union(line_bounds)
        else
            bounds = line_bounds
    }

    def visit_group(group) {
        group.shapes.each { shape -> shape.accept(this) }
    }
}

def group = new Group()
group.add(new Rectangle(100, 40, 10, 5))
group.add(new Rectangle(100, 70, 10, 5))
group.add(new Line(90, 30, 60, 5))
def visitor = new BoundingRectangleVisitor()
group.accept(visitor)
bounding_box = visitor.bounds
println bounding_box.dump()

呃,代码的确有点多。

下面来简化一下,使用 Groovy 的闭包让代码量减半:

abstract class Shape {
    def accept(Closure yield) { yield(this) }
}

class Rectangle extends Shape {
    def x, y, w, h
    def bounds() { this }
    def union(rect) {
        if (!rect) return this
        def minx = [ rect.x, x ].min()
        def maxx = [ rect.x + w, x + w ].max()
        def miny = [ rect.y, y ].min()
        def maxy = [ rect.y + h, y + h ].max()
        new Rectangle(x:minx, y:miny, w:maxx - minx, h:maxy - miny)
    }
}

class Line extends Shape {
    def x1, y1, x2, y2
    def bounds() {
        new Rectangle(x:[x1, x2].min(), y:[y1, y2].min(), w:(x2 - x1).abs(), h:(y2 - y1).abs())
    }
}

class Group {
    def shapes = []
    def leftShift(shape) { shapes += shape }
    def accept(Closure yield) { shapes.each{it.accept(yield)} }
}

def group = new Group()
group << new Rectangle(x:100, y:40, w:10, h:5)
group << new Rectangle(x:100, y:70, w:10, h:5)
group << new Line(x1:90, y1:30, x2:60, y2:5)
def bounds
group.accept{ bounds = it.bounds().union(bounds) }
println bounds.dump()

1.18.2 复杂范例

interface Visitor {
    void visit(NodeType1 n1)
    void visit(NodeType2 n2)
}

interface Visitable {
    void accept(Visitor visitor)
}

class NodeType1 implements Visitable {
    Visitable[] children = new Visitable[0]
    void accept(Visitor visitor) {
        visitor.visit(this)
        for(int i = 0; i < children.length; ++i) {
            children[i].accept(visitor)
        }
    }
}

class NodeType2 implements Visitable {
    Visitable[] children = new Visitable[0]
    void accept(Visitor visitor) {
        visitor.visit(this)
        for(int i = 0; i < children.length; ++i) {
            children[i].accept(visitor)
        }
    }
}

class NodeType1Counter implements Visitor {
    int count = 0
    void visit(NodeType1 n1) {
        count++
    }
    void visit(NodeType2 n2){}
}

如果在一个树上使用 NodeType1Counter

NodeType1 root = new NodeType1()
root.children = new Visitable[2]
root.children[0] = new NodeType1()
root.children[1] = new NodeType2()

那么一个 NodeType1 对象成为根,而其中的一个子节点是一个 NodeType1 实例。另一个子节点是 NodeType2 实例。这意味着使用 NodeType1Counter 应该计为 2 个 NodeType1 对象。

为什么要使用

访问者拥有一个状态,而对象树也没有改变。这一点在很多不同场合都非常有用,比如用一个访问者来计算所有节点类型,或使用了多少不同的类型,或者可以使用该节点专有方法来收集树的信息,等等。

如果加入新类型会出现什么情况?

这种情况下的确需要做更多的工作。必须改变访问者,使其接受新类型,当然必须编写类型本身,必须改变每一个已经实现了的访问者。经过一番改动之后,将所有的访问者修改为扩展自一个访问者的默认实现,所以每当添加一个新的类型时,不需要改变每一个访问者了。

如果想实现不同的迭代模式呢?

这就会出现问题。既然节点描述了如何迭代,那么我们就无法控制在某一点处停止迭代或改变迭代次序。所以我们或许可以稍加改动:

interface Visitor {
    void visit(NodeType1 n1)
    void visit(NodeType2 n2)
}

class DefaultVisitor implements Visitor{
    void visit(NodeType1 n1) {
        for(int i = 0; i < n1.children.length; ++i) {
            n1.children[i].accept(this)
        }
    }
    void visit(NodeType2 n2) {
        for(int i = 0; i < n2.children.length; ++i) {
            n2.children[i].accept(this)
        }
    }
}

interface Visitable {
    void accept(Visitor visitor)
}

class NodeType1 implements Visitable {
    Visitable[] children = new Visitable[0]
    void accept(Visitor visitor) {
        visitor.visit(this)
    }
}

class NodeType2 implements Visitable {
    Visitable[] children = new Visitable[0];
    void accept(Visitor visitor) {
        visitor.visit(this)
    }
}

class NodeType1Counter extends DefaultVisitor {
    int count = 0
    void visit(NodeType1 n1) {
        count++
        super.visit(n1)
    }
}

小变动产生巨大的效果。访问者现在是递归的,能清晰地告诉我们如何迭代。节点上的实现被简化为 visitor.visit(this)DefaultVisitor 现在可以捕获新类型了,从而不必委托给超类,即可停止迭代。当然,现在最大的缺点还是在于不再迭代了,但这是正常的:你无法获得所有的好处,不是吗?

使其 Groovy 化

现在的问题是,如何让它变得更 Groovy 化。你不觉得 visitor.visit(this) 很奇怪吗?这里为什么要出现它?其实,这是对双重分发的一种模拟。Java 使用了编译时类型,因此当 visitor.visit(children[i]),编译器不会发现正确的方法,因为 Visitor 并不包含方法 visit(Visitable)。甚至如果是包含了该方法,我们更乐于利用 NodeType1NodeType2访问更为特殊的方法。

Groovy 并没有使用静态类型,而是运行时类型。这意味着可以直接执行 visitor.visit(children[i])。唔……因为简化了接收方法,使其只执行双重分发部分,而且 Groovy 的运行时类型系统已经涉及到了这一点……难道我们还需要接收方法吗?我觉得你肯定会认为答案是不需要。但还需要详细地解释一下。不知道如何处理未知的树元素,这的确是一个缺点。为此,我们必须扩展 Visitor 接口,从而造成 DefaultVisitor 的更改,继而必须提供一个更有用的默认行为,比如迭代节点或根本什么都不做。添加一个什么都不实现的 visit(Visitable) 方法,从而可以利用 Groovy 来捕获这种情况。顺便说一句,这和 Java 中的做法完全一样。

继续说,我们需要 Visitor 接口吗?如果没有接收方法,那么当然也不需要 Visitor 接口了。因此新代码如下:

class DefaultVisitor {
    void visit(NodeType1 n1) {
        n1.children.each { visit(it) }
    }
    void visit(NodeType2 n2) {
        n2.children.each { visit(it) }
    }
    void visit(Visitable v) { }
}

interface Visitable { }

class NodeType1 implements Visitable {
    Visitable[] children = []
}

class NodeType2 implements Visitable {
    Visitable[] children = []
}

class NodeType1Counter extends DefaultVisitor {
    int count = 0
    void visit(NodeType1 n1) {
        count++
        super.visit(n1)
    }
}

看起来又省去了一些代码。但还可以继续思考下去。Visitable 节点并不引用任何的 Visitor 类或接口。我认为这是目前所能达到的最好的分离级别。接下来,还可以稍微改变一下 Visitable 接口,使其返回下一步想访问的子节点。从而需要实现一个通用的迭代方法:

class DefaultVisitor {
    void visit(Visitable v) {
        doIteraton(v)
    }
    void doIteraton(Visitable v) {
        v.children.each {
            visit(it)
        }
    }
}

interface Visitable {
    Visitable[] getChildren()
}

class NodeType1 implements Visitable {
    Visitable[] children = []
}

class NodeType2 implements Visitable {
    Visitable[] children = []
}

class NodeType1Counter extends DefaultVisitor {
    int count = 0
    void visit(NodeType1 n1) {
        count++
        super.visit(n1)
    }
}

DefaultVisitor 看起来有点奇怪。添加了一个 doIteration 方法,它会获取所有需要迭代的子节点,在每一元素上都调用访问方法。调用将用于迭代该子节点子级的 visit(Visitable)。改变了 Visitable,以便来确保任何节点都能返回子级(甚至为空时)。并不一定要改变 NodeType1NodeType2 类,因为子级已被定义的管理方式使其成为一种属性,从而意味着 Groovy 可以非常好地替我们生成一个 get 方法。真正有意思的是 NodeType1Counter,因为我们未曾修改过它。super.visit(n1) 将调用 visit(Visitable),而后者也将调用开启下一级迭代的 doIteration。因此上无须改变它。但是,如果 it 类型为 NodeType1visit(it) 则将调用 visit(NodeType1)。实际上,我们并不需要 doIteration 方法,可以利用 visit(Visitable) 来实现,但是我认为前者要略好一些,因为在出现错误时,我们可以编写一个新的 Visitor 来重写 visit(Visitable)——在这种情况下,不能用 super.visit(n1),而只能用 doIteration(n1)

小结

最终,我们成功地将代码量削减了大约超过 60%,实现了一种更为健壮而稳定的架构,最终从 Visitable 中清除了访问者。我曾听说,访问者实现可以基于反射,为的是达到更通用的版本。但如你所见,不需要那样做。如果添加了新类型,则不需要改变任何东西。有人认为,访问者模式并不太适用于极限编程技术,因为需要在一个时间段内修改很多类。我认为,出现这种问题的原因在于 Java,模式本身并没有什么好不好的。

访问者模式也存在一些变体形式,像非循环访问者模式,它要解决的问题是如何利用特殊访问者添加新节点类型。我并不是很喜欢这个变体,它基于 Cast,捕获 ClassCastException 和其他一些肮脏的东西。总之,它所试图要解决的问题,我们在 Groovy 版本中根本就碰不到。

另外,NodeType1Counter 也可以用 Java 来实现。Groovy 会识别访问方法,在需要时调用它们,因为 好用的 DefaultVisitor 仍然属于 Groovy。

1.18.3 更多参考资料

2. 参考文献

  1. Design Patterns: Elements of Reusable Object-Oriented Software(1995,Addison-Wesley. ISBN 0-201-63361-2)作者:Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides。中译本为:《设计模式:可复用面向对象软件的基础》。

    • 设计模式方面的权威读物。
  2. Refactoring: Improving the Design of Existing Code(1999,Addison-Wesley。ISBN 0-201-48567-2)作者:Martin Fowler。中译本:《重构:改善既有代码的设计》

  3. Refactoring To Patterns(2004,Addison-Wesley。ISBN 0-321-21335-1)作者:Joshua Kerievsky。中译本为:《重构与模式》

  4. Head First Design Patterns(2004,O’Reilly。ISBN 0-596-00712-4)作者:Eric Freeman、Elisabeth Freeman、Kathy Sierra 和 Bert Bates。中译本为:《Head first 设计模式》

    • 非常值得读的一本书,不仅有益,而且有趣。
  5. Groovy in Action(2007,Manning。ISBN 1-932394-84-2)作者:Dierk Koenig、Andrew Glover、Paul King、Guillaume Laforge 及 Jon Skeet。中译本为:《Groovy 实战》

    • 主要介绍访问者、构建者及其他模式。
  6. Brad Appleton (1999). Pizza Inversion - a Pattern for Efficient Resource Consumption.

    • 这是很多软件工程师使用得最多的一种模式
  7. Design Patterns in Dynamic Languages Neil Ford 著。Houston Java User’s Group. 范例语言使用的是 Groovy 和 Ruby。

    http://www.hjug.org/present/Neal_Ford-Design_Patterns_in_Dynamic_Languages-slides.pdf

上一篇: 安全性 下一篇: 语法风格指南