本文根据 Apache Groovy 官方文档《Design patterns in Groovy》翻译整理。
在 Java 中使用 设计模式 是一个成熟的主题。设计模式也适用于 Groovy:
-
一些模式直接延续(并且可以利用普通的 Groovy 语法改进来提高可读性)
-
某些模式不再需要,因为它们直接内置于语言中,或者因为 Groovy 支持更好的方式来实现模式的意图
-
一些必须在其他语言的设计级别表达的模式可以直接在 Groovy 中实现(由于 Groovy 可以模糊设计和实现之间的区别)
抽象工厂模式
抽象工厂模式提供了一种封装一组具有共同主题的单独工厂的方法。它体现了普通工厂的意图,即不需要使用接口来了解接口背后的具体实现,而是应用于一组接口并选择实现这些接口的整个具体类系列。
例如,我可能有 Button、TextField 和 Scrollbar 接口。我可能有 WindowsButton、MacButton、FlashButton 作为 Button 的具体类。我可能有 WindowsScrollBar、MacScrollBar 和 FlashScrollBar 作为 ScrollBar 的具体实现。使用抽象工厂模式应该允许我选择我想要使用一次的窗口系统(即 Windows、Mac、Flash),从那时起应该能够编写引用接口的代码,但始终在幕后使用适当的具体类(全部来自一个窗口系统)。
示例
假设我们要编写一个游戏系统。我们可能会注意到许多游戏具有非常相似的功能和控制。
我们决定尝试将通用代码和游戏特定代码分成单独的类。
首先让我们看一下 Two-up 游戏的游戏特定代码:
groovy
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'
}
}
}
现在,让我们看一下猜数字游戏的游戏特定代码:
groovy
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()
}
}
}
现在,让我们编写工厂代码:
groovy
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() }
}
该工厂的重要方面是它允许选择整个具体类系列。
以下是我们如何使用工厂:
groovy
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
请注意,第一行配置了我们将使用的具体游戏类系列。我们通过使用工厂属性来选择要使用的系列并不重要,如第一行所示。其他方式也是这种模式的同样有效的例子。例如,我们可能询问用户他们想玩哪个游戏或从环境设置中确定哪个游戏。
使用如图所示的代码,游戏运行时可能如下所示:
text
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,则示例运行可能如下所示:
text
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
适配器模式
适配器模式(有时称为包装器模式)允许在需要另一种类型的接口的地方使用满足一个接口的对象。该模式有两种典型风格:delegation 风格和 inheritance 风格。
委托示例
假设我们有以下类:
groovy
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 属性(即不满足所需的接口)。
为了解决这个问题,我们可以创建一个适配器,使其看起来具有正确的接口。它看起来像这样:
groovy
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)"
}
}
我们可以这样使用适配器:
groovy
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"
}
}
这会产生以下输出:
text
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
继承示例
让我们再次考虑使用继承的同一示例。首先,这是原始类(未更改):
groovy
class SquarePeg {
def width
}
class RoundPeg {
def radius
}
class RoundHole {
def radius
def pegFits(peg) {
peg.radius <= radius
}
String toString() { "RoundHole with radius $radius" }
}
使用继承的适配器:
groovy
class SquarePegAdapter extends SquarePeg {
def getRadius() {
Math.sqrt(((width / 2) ** 2) * 2)
}
String toString() {
"SquarePegAdapter with width $width (and notional radius $radius)"
}
}
使用适配器:
groovy
def hole = new RoundHole(radius: 4.0)
(4..7).each { w ->
def peg = new SquarePegAdapter(width: w)
if (hole.pegFits(peg)) {
println "peg $peg fits in hole $hole"
} else {
println "peg $peg does not fit in hole $hole"
}
}
输出:
text
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
使用闭包进行适配
作为前面示例的变体,我们可以定义以下接口:
groovy
interface RoundThing {
def getRadius()
}
然后我们可以将适配器定义为闭包,如下所示:
groovy
def adapter = {
p -> [getRadius: { Math.sqrt(((p.width / 2) ** 2) * 2) }] as RoundThing
}
并像这样使用它:
groovy
def peg = new SquarePeg(width: 4)
if (hole.pegFits(adapter(peg))) {
// ... as before
}
使用 ExpandoMetaClass 进行适配
从 Groovy 1.1 开始,有一个内置的 MetaClass 可以自动动态添加属性和方法。
以下是该示例如何使用该功能:
groovy
def peg = new SquarePeg(width: 4)
peg.metaClass.radius = Math.sqrt(((peg.width / 2) ** 2) * 2)
创建钉子对象后,您可以简单地动态向其添加属性。无需更改原始类,也无需适配器类。
Bouncer 模式
Bouncer 模式 描述了一种方法的用法,该方法的唯一目的是抛出异常(当特定条件成立时)或不执行任何操作。此类方法通常用于防御性地保护方法的先决条件。
在编写实用程序方法时,您应该始终防止错误的输入参数。在编写内部方法时,您可以通过进行足够的单元测试来确保某些先决条件始终成立。在这种情况下,您可能会降低对您的方法设置防护的需求。
Groovy 与其他语言的不同之处在于,您经常在方法中使用 assert 方法,而不是拥有大量实用程序检查器方法或类。
空值检查示例
我们可能有一个实用方法,例如:
groovy
class NullChecker {
static check(name, arg) {
if (arg == null) {
throw new IllegalArgumentException(name + ' is null')
}
}
}
我们会这样使用它:
groovy
void doStuff(String name, Object value) {
NullChecker.check('name', name)
NullChecker.check('value', value)
// do stuff
}
但更 Groovy 的方式来做到这一点就像这样:
groovy
void doStuff(String name, Object value) {
assert name != null, 'name should not be null'
assert value != null, 'value should not be null'
// do stuff
}
验证示例
作为替代示例,我们可能有以下实用方法:
groovy
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')
}
}
}
我们会这样使用它:
groovy
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,我们就可以轻松使用:
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
}
责任链模式
在责任链模式中,使用和实现接口(一个或多个方法)的对象有意地松散耦合。一组 implement 接口的对象被组织在一个列表中(或者在极少数情况下是一个树)。使用该接口的对象从第一个 implementor 对象发出请求。它将决定是否自行执行任何操作以及是否将请求进一步传递到列表(或树)中的行中。有时,如果没有实现者响应请求,则某些请求的默认实现也会被编码到模式中。
使用传统类的示例
在此示例中,脚本将请求发送到 lister 对象。 lister 指向 UnixLister 对象。如果它无法处理该请求,则会将请求发送到 WindowsLister。如果它无法处理该请求,则会将请求发送到 DefaultLister。
groovy
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').startsWith('Windows')) {
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 表示:
plantuml
!pragma layout smetana
skinparam nodesep 200
skinparam ObjectBorderColor<<Hidden>> Transparent
skinparam ObjectBackgroundColor<<Hidden>> Transparent
skinparam ObjectFontColor<<Hidden>> Transparent
skinparam ObjectStereotypeFontColor<<Hidden>> Transparent
class UnixLister {
nextInLine : Object
+listFiles(dir)
}
object dummy1<<Hidden>>
class WindowsLister {
nextInLine : Object
+listFiles(dir)
}
object dummy2<<Hidden>>
class DefaultLister {
+listFiles(dir)
}
hide DefaultLister fields
object script
script ..r..> "<<use>>" UnixLister
UnixLister --> "forwardIfRequired" WindowsLister
UnixLister <-[hidden]- dummy1
WindowsLister <-[hidden]- dummy2
WindowsLister --> "forwardIfRequired" DefaultLister
hide <<Hidden>>
使用简化策略的示例
对于简单的情况,请考虑通过不需要类链来简化代码。相反,请使用 Groovy truth 和 elvis 运算符,如下所示:
groovy
String unixListFiles(dir) {
if (System.getProperty('os.name') == 'Linux') {
"ls $dir".execute().text
}
}
String windowsListFiles(dir) {
if (System.getProperty('os.name').startsWith('Windows')) {
"cmd.exe /c dir $dir".execute().text
}
}
String defaultListFiles(dir) {
new File(dir).listFiles().collect{ f -> f.name }.join('\\n')
}
def dir = 'Downloads'
println unixListFiles(dir) ?: windowsListFiles(dir) ?: defaultListFiles(dir)
或者 Groovy 的开关如下所示:
groovy
String listFiles(dir) {
switch(dir) {
case { System.getProperty('os.name') == 'Linux' }:
return "ls $dir".execute().text
case { System.getProperty('os.name').startsWith('Windows') }:
return "cmd.exe /c dir $dir".execute().text
default:
new File(dir).listFiles().collect{ f -> f.name }.join('\\n')
}
}
println listFiles('Downloads')
或者,对于 Groovy 3+,请考虑使用 lambda 流,如下所示:
groovy
Optional<String> unixListFiles(String dir) {
Optional.ofNullable(dir)
.filter(d -> System.getProperty('os.name') == 'Linux')
.map(d -> "ls $d".execute().text)
}
Optional<String> windowsListFiles(String dir) {
Optional.ofNullable(dir)
.filter(d -> System.getProperty('os.name').startsWith('Windows'))
.map(d -> "cmd.exe /c dir $d".execute().text)
}
Optional<String> defaultListFiles(String dir) {
Optional.ofNullable(dir)
.map(d -> new File(d).listFiles().collect{ f -> f.name }.join('\\n'))
}
def dir = 'Downloads'
def handlers = [this::unixListFiles, this::windowsListFiles, this::defaultListFiles]
println handlers.stream()
.map(f -> f(dir))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.get()
不应使用的情况
如果您使用责任链涉及频繁使用 instanceof 运算符,如下所示:
groovy
import static Math.PI as π
abstract class Shape {
String name
}
class Polygon extends Shape {
String name
double lengthSide
int numSides
}
class Circle extends Shape {
double radius
}
class CircleAreaCalculator {
def next
def area(shape) {
if (shape instanceof Circle) { // <1>
return shape.radius ** 2 * π
} else {
next.area(shape)
}
}
}
class SquareAreaCalculator {
def next
def area(shape) {
if (shape instanceof Polygon && shape.numSides == 4) { // <1>
return shape.lengthSide ** 2
} else {
next.area(shape)
}
}
}
class DefaultAreaCalculator {
def area(shape) {
throw new IllegalArgumentException("Don't know how to calculate area for $shape")
}
}
def chain = new CircleAreaCalculator(next: new SquareAreaCalculator(next: new DefaultAreaCalculator()))
def shapes = [
new Circle(name: 'Circle', radius: 5.0),
new Polygon(name: 'Square', lengthSide: 10.0, numSides: 4)
]
shapes.each { println chain.area(it) }
- 代码味道实例
它可能表明您可以考虑使用更丰富的类型,也许与 Groovy 的多方法结合使用,而不是使用责任链模式。例如,也许是这样的:
groovy
// ...
class Square extends Polygon {
// ...
}
double area(Circle c) {
c.radius ** 2 * π
}
double area(Square s) {
s.lengthSide ** 2
}
def shapes = [
new Circle(radius: 5.0),
new Square(lengthSide: 10.0, numSides: 4)
]
shapes.each { println area(it) }
或者使用更传统的面向对象风格,如下所示:
groovy
import static Math.PI as π
interface Shape {
double area()
}
abstract class Polygon implements Shape {
double lengthSide
int numSides
abstract double area()
}
class Circle implements Shape {
double radius
double area() {
radius ** 2 * π
}
}
class Square extends Polygon {
// ...
double area() {
lengthSide ** 2
}
}
def shapes = [
new Circle(radius: 5.0),
new Square(lengthSide: 10.0, numSides: 4)
]
shapes.each { println it.area() }
进一步探索
此模式的其他变体:
-
在传统的例子中我们可以有一个显式的接口,例如
Lister,静态键入实现,但由于_duck-typing_,这是可选的 -
我们可以使用链树而不是列表,例如
if (animal.hasBackbone())委托给VertebrateHandler,否则委托给InvertebrateHandler -
即使我们处理了请求,我们也总是可以沿着链传递(不会提前返回)
-
我们可以在某个时刻决定不响应并且不向下传递链(先发制人中止)
-
我们可以使用 Groovy 的元编程功能将未知的方法传递到链上,例如将责任链与
methodMissing的使用结合起来
命令模式
命令模式 是一种松散耦合想要执行一系列命令的客户端对象和执行这些命令的接收者对象的模式。客户端不是直接与接收者对话,而是与中间对象交互,然后中间对象将必要的命令转发给接收者。该模式在 JDK 中很常见,例如 Swing 中的 javax.swing.Action 类将 swing 代码与按钮、菜单项和面板等接收器解耦。
显示典型类的类图是:
plantuml
!pragma layout smetana
skinparam nodesep 100
hide fields
interface Command {
+execute(String command)
}
object client
class Command1 implements Command {
+execute(String command)
}
class Receiver1 {
+action1(args1)
}
client ..r..> "command" Command
Command1 --r--> Receiver1
对于任意接收者,交互顺序如下所示:
plantuml
!pragma layout smetana
client -> intermediary: command
intermediary -> receiverN: actionN
使用传统类的示例
打开和关闭灯所需的相关类(请参阅早期维基百科参考中的示例)如下:
groovy
interface Command {
void execute()
}
// invoker class
class Switch {
private final Map<String, Command> commandMap = new HashMap<>()
void register(String commandName, Command command) {
commandMap[commandName] = command
}
void execute(String commandName) {
Command command = commandMap[commandName]
if (!command) {
throw new IllegalStateException("no command registered for " + commandName)
}
command.execute()
}
}
// receiver class
class Light {
void turnOn() {
println "The light is on"
}
void turnOff() {
println "The light is off"
}
}
class SwitchOnCommand implements Command {
Light light
@Override // Command
void execute() {
light.turnOn()
}
}
class SwitchOffCommand implements Command {
Light light
@Override // Command
void execute() {
light.turnOff()
}
}
Light lamp = new Light()
Command switchOn = new SwitchOnCommand(light: lamp)
Command switchOff = new SwitchOffCommand(light: lamp)
Switch mySwitch = new Switch()
mySwitch.register("on", switchOn)
mySwitch.register("off", switchOff)
mySwitch.execute("on")
mySwitch.execute("off")
我们的客户端脚本向中介发送 execute 命令,并且对任何特定接收者或任何特定操作方法名称和参数一无所知。
简化变体
鉴于 Groovy 具有一流的函数支持,我们可以通过使用闭包来取消实际的命令类(如 SwitchOnCommand),如下所示:
groovy
interface Command {
void execute()
}
// invoker class
class Switch {
private final Map<String, Command> commandMap = [:]
void register(String commandName, Command command) {
commandMap[commandName] = command
}
void execute(String commandName) {
Command command = commandMap[commandName]
if (!command) {
throw new IllegalStateException("no command registered for $commandName")
}
command.execute()
}
}
// receiver class
class Light {
void turnOn() {
println 'The light is on'
}
void turnOff() {
println 'The light is off'
}
}
Light lamp = new Light()
Switch mySwitch = new Switch()
mySwitch.register("on", lamp.&turnOn) // <1>
mySwitch.register("off", lamp.&turnOff) // <1>
mySwitch.execute("on")
mySwitch.execute("off")
- 命令闭包(这里是方法闭包),但可以是 Groovy 3+ 的 lambdas/方法引用
我们可以使用 JDK 现有的 Runnable 接口并使用 switch 映射来进一步简化,而不是单独的 Switch 类,如下所示:
groovy
class Light {
void turnOn() {
println 'The light is on'
}
void turnOff() {
println 'The light is off'
}
}
class Door {
static void unlock() {
println 'The door is unlocked'
}
}
Light lamp = new Light()
Map<String, Runnable> mySwitch = [
on: lamp::turnOn,
off: lamp::turnOff,
unlock: Door::unlock
]
mySwitch.on()
mySwitch.off()
mySwitch.unlock()
我们添加了一个额外的 Door 接收器来说明如何扩展原始示例。运行此脚本会导致:
text
The light is on
The light is off
The door is unlocked
作为一种变体,如果命令名称对我们来说不重要,我们可以放弃使用切换映射,而只使用要调用的任务列表,如下所示:
groovy
// ...
List<Runnable> tasks = [lamp::turnOn, lamp::turnOff, Door::unlock]
tasks.each{ it.run() }
组合模式
复合模式 允许您以与一组对象相同的方式处理对象的单个实例。该模式通常与对象的层次结构一起使用。通常,对于层次结构中的 leaf 或 composite 节点,应该可以以相同的方式调用一个或多个方法。在这种情况下,复合节点通常为其每个子节点调用相同的命名方法。
示例
考虑复合模式的这种用法,我们希望在 Leaf 或 Composite 对象上调用 toString()。
plantuml
!pragma layout smetana
skinparam linetype ortho
skinparam nodesep 100
class Component {
+toString()
}
class Leaf {
+toString()
}
class Composite {
+toString()
}
object componentClient
hide fields
componentClient ..r..> "<<use>>" Component
Component <|-- Leaf
Composite "1" *-- "*" Component : children
Component <|-- Composite
在 Java 中,Component 类至关重要,因为它提供了用于叶节点和复合节点的类型。在 Groovy 中,由于鸭子类型,我们不需要它来实现此目的,但是,它仍然可以作为在叶节点和复合节点之间放置常见行为的有用位置。
出于我们的目的,我们将组装以下组件层次结构。
plantuml
!pragma layout smetana
object root
object "leaf A" as leafA
object "comp B" as compB
object "leaf C" as leafC
object "leaf B1" as leafB1
object "leaf B2" as leafB2
root -- leafA
root -- compB
root -- leafC
compB -- leafB1
compB -- leafB2
这是代码:
groovy
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)
这是结果输出:
text
root
-leaf A
-comp B
--leaf B1
--leaf B2
-leaf C
装饰器模式
装饰器模式提供了一种修饰对象行为而不改变其基本接口的机制。装饰对象应该能够在任何需要原始(未装饰)对象的地方进行替换。装饰通常不涉及修改原始对象的源代码,并且装饰器应该能够以灵活的方式组合以生成具有多种装饰的对象。
传统示例
假设我们有以下 Logger 类。
groovy
class Logger {
def log(String message) {
println message
}
}
有时,对日志消息添加时间戳可能很有用,或者有时我们可能想要更改消息的大小写。我们可以尝试将所有这些功能构建到我们的 Logger 类中。如果我们这样做,Logger 类将开始变得非常复杂。此外,每个人都可以获得所有功能,即使他们可能只需要功能的一小部分。最后,功能交互将变得相当难以控制。
为了克服这些缺点,我们定义了两个装饰器类。 Logger 类的使用可以自由地用零个或多个装饰器类以任何他们想要的顺序来修饰他们的基本记录器。这些类看起来像这样:
groovy
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())
}
}
我们可以像这样使用装饰器:
groovy
def logger = new UpperLogger(new TimeStampingLogger(new Logger()))
logger.log("G'day Mate")
// => Tue May 22 07:13:50 EST 2007: G'DAY MATE
您可以看到我们用两个装饰器来修饰记录器行为。由于我们选择应用装饰器的顺序,我们的日志消息以大写形式显示,并且时间戳是正常情况下的。如果我们交换顺序,让我们看看会发生什么:
groovy
logger = new TimeStampingLogger(new UpperLogger(new Logger()))
logger.log('Hi There')
// => TUE MAY 22 07:13:50 EST 2007: HI THERE
现在时间戳本身也已更改为大写。
使用闭包或 lambda 简化
闭包使代码的表示变得容易。我们可以利用这个事实来创建一个通用的记录器类,它接受装饰代码作为闭包。这节省了我们定义许多装饰类的麻烦。
groovy
class DecoratingLogger {
def decoration = Closure.IDENTITY
def log(String message) {
println decoration(message)
}
}
def upper = { it.toUpperCase() }
def stamp = { "$Calendar.instance.time: $it" }
def logger = new DecoratingLogger(decoration: stamp << upper)
logger.log("G'day Mate")
// Sat Aug 29 15:28:29 AEST 2020: G'DAY MATE
我们可以对 lambda 使用相同的方法:
groovy
import java.util.function.Function
class DecoratingLogger {
Function<String, String> decoration = Function.identity()
def log(String message) {
println decoration.apply(message)
}
}
Function<String, String> upper = s -> s.toUpperCase()
Function<String, String> stamp = s -> "$Calendar.instance.time: $s"
def logger = new DecoratingLogger(decoration: upper.andThen(stamp))
logger.log("G'day Mate")
// => Sat Aug 29 15:38:28 AEST 2020: G'DAY MATE
一点动态行为
我们以前的装饰器特定于 Logger 对象。我们可以使用 Groovy 的元对象编程功能来创建一个本质上更通用的装饰器。考虑这个类:
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 方法参数将自动更改为小写。
groovy
logger = new GenericLowerDecorator(new TimeStampingLogger(new Logger()))
logger.log('IMPORTANT Message')
// => Tue May 22 07:27:18 EST 2007: important message
在这里订购要小心。最初的装饰器仅限于装饰 Logger 对象。该装饰器适用于任何对象类型,因此我们无法交换顺序,即这不起作用:
text
// Can't mix and match Interface-Oriented and Generic decorators
// logger = new TimeStampingLogger(new GenericLowerDecorator(new Logger()))
我们可以通过在运行时生成适当的代理类型来克服此限制,但我们不会使此处的示例复杂化。
运行时行为增强
您还可以考虑使用 Groovy 1.1 中的 ExpandoMetaClass 来动态修饰类的行为。这不是装饰器模式的正常使用方式(它当然没有那么灵活),但在某些情况下可以帮助您获得类似的结果,而无需创建新类。
代码如下所示:
groovy
// current mechanism to enable ExpandoMetaClass
GroovySystem.metaClassRegistry.metaClassCreationHandle = new ExpandoMetaClassCreationHandle()
def logger = new Logger()
logger.metaClass.log = { String m -> println 'message: ' + m.toUpperCase() }
logger.log('x')
// => message: X
这实现了与应用单个装饰器类似的结果,但我们无法轻松地动态应用和删除装饰。
更动态的装饰
假设我们有一个计算器类(实际上任何类都可以)。
groovy
class Calc {
def add(a, b) { a + b }
}
我们可能有兴趣观察该类随时间的使用情况。如果它深埋在我们的代码库中,则可能很难确定它何时被调用以及使用什么参数。此外,可能很难知道它是否表现良好。我们可以轻松地创建一个通用跟踪装饰器,每当调用 Calc 类上的任何方法时,它都会打印出跟踪信息,并且还提供有关执行时间的计时信息。这是跟踪装饰器的代码:
groovy
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
}
}
以下是如何在脚本中使用该类:
groovy
def tracedCalc = new TracingDecorator(new Calc())
assert 15 == tracedCalc.add(3, 12)
运行此脚本后您将看到以下内容:
text
Calling add{3, 12}
Got 15 in 31 ms
使用拦截器进行装饰
上面的计时示例挂钩了 Groovy 对象的生命周期(通过 invokeMethod)。这是一种执行元编程的重要风格,以至于 Groovy 对使用 interceptors 的装饰风格提供了特殊支持。
Groovy 甚至还带有内置的 TracingInterceptor。我们可以像这样扩展内置类:
groovy
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
}
}
以下是使用这个新类的示例:
groovy
def proxy = ProxyMetaClass.getInstance(Calc)
proxy.interceptor = new TimingInterceptor()
proxy.use {
assert 7 == new Calc().add(1, 6)
}
这是输出:
text
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
使用 java.lang.reflect.Proxy 进行装饰
如果您尝试装饰一个对象(即,只是类的特定实例,而不是一般类),那么您可以使用 Java 的 java.lang.reflect.Proxy。 Groovy 使得使用它比 Java 更容易。下面是从 grails 项目中取出的代码示例,该项目包装了 java.sql.Connection,因此它的 close 方法是无操作的:
groovy
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)
}
如果有很多方法需要拦截,那么可以修改此方法以按方法名称在映射中查找闭包并调用它。
使用 Spring 进行装饰
Spring Framework 允许装饰器与 interceptors 一起应用(您可能听说过术语 advice 或 aspect)。您也可以利用 Groovy 中的这种机制。
首先定义一个您想要装饰的类(我们还将像正常的 Spring 实践一样使用接口):
这是界面:
groovy
interface Calc {
def add(a, b)
}
这是类:
groovy
class CalcImpl implements Calc {
def add(a, b) { a + b }
}
现在,我们在名为 beans.xml 的文件中定义接线,如下所示:
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 https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang https://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>
现在,我们的脚本如下所示:
groovy
@Grab('org.springframework:spring-context:5.2.8.RELEASE')
import org.springframework.context.support.ClassPathXmlApplicationContext
def ctx = new ClassPathXmlApplicationContext('beans.xml')
def calc = ctx.getBean('calc')
println calc.add(3, 25)
当我们运行它时,我们会看到结果:
text
21/05/2007 23:02:35 org.springframework.aop.interceptor.PerformanceMonitorInterceptor invokeUnderTrace
FINEST: StopWatch 'util.Calc.add': running time (millis) = 16
您可能需要调整 logging.properties 文件才能显示日志级别 FINEST 的消息。
使用 GPars 的异步装饰器
以下示例的灵感来自 Panini 编程语言的一些早期示例代码。如今,您将看到这种样式与 JavaScript 中的异步函数一起使用。
groovy
import groovy.concurrent.Awaitable
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: {
def result = Awaitable.go { c(d) }
d.print()
println await(result)
}], [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()
委托模式
委托模式是一种通过将责任委托给一个或多个关联对象来实现对象行为(公共方法)的技术。
Groovy 允许应用委托模式的传统风格,例如请参阅 用委托替换继承。
使用 ExpandoMetaClass 实现委托模式
groovy.lang.ExpandoMetaClass 允许将此模式的使用封装在库中。这允许 Groovy 模拟可用于 Ruby 语言的类似库。
考虑以下库类:
groovy
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) }
}
}
通过将其添加到类路径中,您现在可以动态应用委托模式,如以下示例所示。首先,考虑我们有以下类:
groovy
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 类。我们可以按原样借用方法或重命名方法:
groovy
delegator.delegate 'borrowFor'
delegator.delegate 'borrowAmount', 'getMoney'
def p = new Person()
println p.borrowFor('present') // => buy present
println p.getMoney(50)
上面的第一行通过委托给 lender 对象,将 borrowFor 方法添加到 Person 类。第二行通过委托给 lender 对象的 borrowAmount 方法,将 getMoney 方法添加到 Person 类中。
或者,我们可以借用多种方法,如下所示:
groovy
delegator.delegateAll 'borrowFor', 'borrowAmount'
它将这两个方法添加到 Person 类中。
或者,如果我们想要所有方法,如下所示:
groovy
delegator.delegateAll()
这将使委托对象中的所有方法在 Person 类中可用。
或者,我们可以使用映射符号来重命名多个方法:
groovy
delegator.delegateAll borrowAmount:'getMoney', borrowFor:'getThing'
使用 @Delegate 注解实现委托模式
从 1.6 版本开始,您可以使用基于 AST 转换的内置委托机制。
这使得委派变得更加容易:
groovy
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)
享元模式
Flyweight 模式 是一种在处理包含许多基本相同内容的系统时不需要创建大量重量级对象来大大减少内存需求的模式。例如,如果使用了解 unicode、字体、定位等的复杂字符类对文档进行建模,那么如果文档中的每个物理字符都需要其自己的字符类实例,则对于大型文档来说,内存需求可能会相当大。相反,字符本身可能保存在字符串中,并且我们可能有一个字符类(或一小部分,例如每种字体类型都有一个字符类)知道如何处理字符的细节。
在这种情况下,我们将与许多其他事物(例如字符类型)共享的状态称为 intrinsic 状态。它属于重量级。区分物理字符(可能只是其 ASCII 代码或 Unicode)的状态称为其 extrinsic 状态。
示例
首先,我们将对一些复杂的飞机进行建模(第一个是第二个的恶作剧竞争对手 - 尽管这与示例无关)。
groovy
class Boeing797 {
def wingspan = '80.8 m'
def capacity = 1000
def speed = '1046 km/h'
def range = '14400 km'
// ...
}
groovy
class Airbus380 {
def wingspan = '79.8 m'
def capacity = 555
def speed = '912 km/h'
def range = '10370 km'
// ...
}
如果我们想对我们的舰队进行建模,我们的第一次尝试可能涉及使用这些重型物体的许多实例。事实证明,每架飞机只有一小部分状态(我们的外部状态)发生变化,因此我们将为重量级物体设置单例,并分别捕获外部状态(下面代码中的购买日期和资产编号)。
groovy
class FlyweightFactory {
static instances = [797: new Boeing797(), 380: new Airbus380()]
}
class Aircraft {
private type // intrinsic state
private assetNumber // extrinsic state
private bought // extrinsic state
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() }
因此,即使我们的机队有数百架飞机,每种类型的飞机也只有一个重型物体。
作为进一步的效率措施,我们可以使用延迟创建享元对象,而不是像上面的示例那样预先创建初始映射。
运行此脚本会导致:
text
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
迭代器模式
迭代器模式允许顺序访问聚合对象的元素,而不暴露其底层表示。
Groovy 的许多闭包运算符中都内置了迭代器模式,例如each 和 eachWithIndex 以及 for .. in 循环。
例如:
groovy
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
输出结果:
text
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]
另一个例子:
groovy
colors.eachWithIndex { item, pos ->
println "Position $pos contains '$item'"
}
结果:
text
Position 0 contains 'java.awt.Color[r=0,g=0,b=0]'
Position 1 contains 'java.awt.Color[r=255,g=255,b=255]'
迭代器模式还内置于其他特殊运算符中,例如 eachByte、eachFile、eachDir、eachLine、eachObject、eachMatch 运算符,用于处理流、URL、文件、目录和正则表达式匹配。
资源借用模式
借出我的资源 模式可确保资源在超出范围后被确定性地处置。
此模式内置于许多 Groovy 辅助方法中。如果您需要以 Groovy 支持之外的方式使用资源,您应该考虑自己使用它。
示例
考虑以下处理文件的代码。首先,我们可以向文件写入一些行,然后打印其大小:
groovy
def f = new File('junk.txt')
f.withPrintWriter { pw ->
pw.println(new Date())
pw.println(this.class.name)
}
println f.size()
// => 42
我们还可以一次读回文件的内容并打印每一行:
groovy
f.eachLine { line ->
println line
}
// =>
// Mon Jun 18 22:38:17 EST 2007
// RunPattern
请注意,Groovy 在幕后使用了普通的 Java Reader 和 PrintWriter 对象,但代码编写者不必担心显式创建或关闭这些资源。内置的 Groovy 方法将相应的读取器或写入器借给闭包代码,然后自行清理。因此,您无需执行任何工作即可使用此模式。
然而,有时您希望做的事情与使用 Groovy 的内置机制免费获得的事情略有不同。您应该考虑在您自己的资源处理操作中使用此模式。
考虑如何处理文件中每一行的单词列表。实际上,我们也可以使用 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 块中,我们就会面临使文件句柄保持打开状态的风险。
现在让我们应用贷款模式。首先,我们将编写一个辅助方法:
groovy
def withListOfWordsForEachLine(File f, Closure c) {
def r = f.newReader()
try {
r.splitEachLine(' ', c)
} finally {
r?.close()
}
}
现在,我们可以重写我们的代码,如下所示:
groovy
withListOfWordsForEachLine(f) { wordList ->
println wordList
}
// =>
// [ "Mon", "Jun", "18", "22:38:17", "EST", "2007" ]
// [ "RunPattern" ]
这要简单得多,并且删除了显式的 close()。现在,这一点已在一个地方得到满足,因此我们可以在一个地方进行适当级别的测试或审查,以确保没有问题。
使用 Monoids
Monoids 允许聚合算法的机制与与该聚合相关的特定于算法的逻辑分离。它通常被认为是一种功能性设计模式。
也许通过一个例子最容易看出这一点。考虑整数和、整数积和字符串连接的代码。我们可能会注意到各种相似之处:
groovy
def nums = [1, 2, 3, 4]
def sum = 0 // <1>
for (num in nums) { sum += num } // <2>
assert sum == 10
def product = 1 // <1>
for (num in nums) { product *= num } // <2>
assert product == 24
def letters = ['a', 'b', 'c']
def concat = '' // <1>
for (letter in letters) { concat += letter } // <2>
assert concat == 'abc'
-
初始化聚合计数器
-
带有 for/while/iteration 调整计数器的循环抛出元素
我们可以删除重复的聚合编码,并梳理出每种算法的重要差异。我们可以改用 Groovy 的 inject 方法。这是函数式编程术语中的"折叠"操作。
groovy
assert nums.inject(0){ total, next -> total + next } == 10
assert nums.inject(1){ total, next -> total * next } == 24
assert letters.inject(''){ total, next -> total + next } == 'abc'
这里的第一个参数是初始值,提供的闭包包含特定于算法的逻辑。
同样,对于 Groovy 3+,我们可以使用 JDK 流 API 和 lambda 语法,如下所示:
groovy
assert nums.stream().reduce(0, (total, next) -> total + next) == 10
assert nums.stream().reduce(1, (total, next) -> total * next) == 24
assert letters.stream().reduce('', (total, next) -> total + next) == 'abc'
稍作形式化
看看这些示例,我们可能会认为所有聚合都可以通过这种方式支持。事实上,我们寻找某些特征来确保此聚合模式适用:
- 关闭:执行聚合步骤应该产生与被聚合的元素类型相同的结果。
示例:
1L + 3L生成Long,'foo' + 'bar'生成String。 + 非 Monoids 示例:'foo'.size() + 'bar'.size()(接受字符串,返回整数),关于加法的_奇数_类型,不处理空参数(如果此类参数可能)的算法。
注意
此处使用术语 closure 时,我们只是指操作下的关闭,而不是 GroovyClosure类。
- 关联性:我们应用聚合步骤的顺序并不重要。
示例:
(1 + 3) + 5与1 + (3 + 5)相同,('a' + 'b') + 'c'与'a' + ('b' + 'c')相同。 + 非 Monoids 示例:(10 - 5) - 3不等于10 - (5 - 3),因此整数在减法方面不是 Monoids 。
- 单位元素(有时也称为"零"元素):应该有一个元素与任何元素聚合返回原始元素。
示例:
0 + 42 == 42、42 + 0 == 42、1 * 42 == 42和'' + 'foo' == 'foo'。 + 非 Monoids 示例:非空字符串 类型就串联而言不是一个 Monoids 。
如果您的算法不满足所有 Monoids 属性,这并不意味着聚合是不可能的。这只是意味着您不会从 Monoids 中获得所有好处(我们将很快介绍),或者您可能还有更多工作要做。此外,您也许可以稍微转换您的数据结构,将您的问题转化为涉及 Monoids 的问题。我们将在本节稍后讨论该主题。
Monoids 的好处
考虑将整数 10 到 16 相加。因为整数的加法运算是 Monoids ,所以我们已经知道我们可以节省编写代码,而是使用我们在前面的 inject 示例中看到的方法。还有一些其他不错的属性。
由于 closure 属性,如果我们有像 sum(Integer a, Integer b) 这样的成对方法,那么对于 Monoids ,我们始终可以扩展该方法以处理列表,例如sum(List<Integer> nums) 或 sum(Integer first, Integer... rest)。
由于_关联性_,我们可以采用一些有趣的方法来解决聚合问题,包括:
-
分治算法将问题分解为更小的部分
-
各种增量算法(例如记忆化)将允许从 1..5 开始求和,并可能通过重用求和 1..4 的缓存值(如果之前已经计算过)来开始计算
-
固有的并行化可以利用多个核心
让我们更详细地看看其中的第一个。对于多核处理器,一个内核可以添加 10 加 11,另一个内核可以添加 12 加 13,依此类推。如果需要,我们将使用 identity 元素(在我们的示例中显示为添加到 16 中)。然后中间结果也可以同时相加在一起,依此类推,直到得到结果。
plantuml
!pragma layout smetana
skinparam shadowing false
skinparam ClassFontSize 18
skinparam ClassBackgroundColor<<Identity>> Transparent
skinparam ClassBorderColor<<Identity>> grey
skinparam ClassStereotypeFontSize<<Identity>> 4
skinparam ClassStereotypeFontColor<<Identity>> Transparent
hide circle
class " 0 " as zero << (I,lightgrey) Identity >>
show zero circle
class " 10 " as a1
class " 11 " as a2
class " 12 " as a3
class " 13 " as a4
class " 14 " as a5
class " 15 " as a6
class " 16 " as a7
class " 21 " as b1
class " 25 " as b2
class " 29 " as b3
class " 16 " as b4
class " 46 " as c1
class " 45 " as c2
class " 91 " as d1
a1 .r[hidden].> a2
a2 .r[hidden].> a3
a3 .r[hidden].> a4
a4 .r[hidden].> a5
a5 .r[hidden].> a6
a6 .r[hidden].> a7
a7 .r[hidden].> zero
b1 <.d. a1
b1 <.d. a2
b2 <.d. a3
b2 <.d. a4
b3 <.d. a5
b3 <.d. a6
b4 <.d. a7
b4 <.d. zero
c1 <.d. b1
c1 <.d. b2
c2 <.d. b3
c2 <.d. b4
d1 <.d. c1
d1 <.d. c2
hide empty members
我们减少了需要编写的代码量,并且还获得了潜在的性能提升。
以下是我们如何使用 Groovy 的并行集合对前面的示例进行编码:
groovy
def nums = 10..16
ParallelScope.withPool(Pool.cpu()) {
assert 91 == nums.toList().injectParallel(0){ total, next -> total + next }
}
处理非 Monoids
假设我们想要求数字 1..10 的平均值。 Groovy 为此提供了一个内置方法:
groovy
assert (1..10).average() == 5.5
现在,假设我们想要构建自己的 Monoids 解决方案,而不是使用内置版本。找到 identity 元素似乎很困难。毕竟:
groovy
assert (0..10).average() == 5
类似地,如果我们想编写成对聚合闭包,它可能类似于:
groovy
def avg = { a, b -> (a + b) / 2 }
我们可以使用什么 b 作为这里的 identity 元素,以便我们的方程返回原始值?我们需要使用 a,但这不是固定值,因此没有_identity_。
此外,关联性不适用于定义 avg 的初始尝试,如以下示例所示:
groovy
assert 6 == avg(avg(10, 2), 6)
assert 7 == avg(10, avg(2, 6))
另外,我们的 closure 属性呢?我们的原始数字是整数,但我们的平均值 (5.5) 不是。我们可以通过对任何 Number 实例进行平均工作来解决这个问题,但这可能并不总是那么容易。
这个问题似乎不适用于 Monoids 解决方案。然而,有多种方法可以将 Monoids 引入解决方案中。
我们可以将其分为两部分:
groovy
def nums = 1..10
def total = nums.sum()
def avg = total / nums.size()
assert avg == 5.5
sum()的计算可以遵循 Monoids 规则,然后我们的最后一步可以计算平均值。我们甚至可以做一个并行版本:
groovy
ParallelScope.withPool(Pool.cpu()) {
assert 5.5 == nums.toList().sumParallel{ a, b -> a + b } / nums.size()
}
在这里,我们使用内置的 sum() 方法(并行示例使用 sumParallel()),但如果您手动执行此操作,则计算的该部分的 Monoids 性质将使您更容易为该步骤编写自己的代码。
或者,我们可以引入一个辅助数据结构,将问题转化为 Monoids 。我们不只是保留总数,而是保留一个包含总数和数字计数的列表。代码可能如下所示:
groovy
def holder = nums
.collect{ [it, 1] }
.inject{ a, b -> [a[0] + b[0], a[1] + b[1]] }
def avg = holder[0] / holder[1]
assert avg == 5.5
或者,更奇特一点,我们可以为我们的数据结构引入一个类,甚至可以并行计算:
groovy
class AverageHolder {
int total
int count
AverageHolder plus(AverageHolder other) {
return new AverageHolder(total: total + other.total,
count: count + other.count)
}
static final AverageHolder ZERO =
new AverageHolder(total: 0, count: 0)
}
def asHolder = {
it instanceof Integer ? new AverageHolder(total: it, count : 1) : it
}
def pairwiseAggregate = { aggregate, next ->
asHolder(aggregate) + asHolder(next)
}
ParallelScope.withPool(Pool.cpu()) {
def holder = nums.toList().injectParallel(AverageHolder.ZERO, pairwiseAggregate)
def avg = holder.with{ total / count }
assert avg == 5.5
}
空对象模式
空对象模式涉及使用表示 null 的特殊对象位置标记对象。通常,如果您引用 null,则无法调用 reference.field 或 reference.method() 您会收到可怕的 NullPointerException。空对象模式使用表示空的特殊对象,而不是使用实际的 null。这允许您调用空对象上的字段和方法引用。使用 null 对象的结果在语义上应该等同于 doing Nothing。
简单示例
假设我们有以下系统:
groovy
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。假设现在我们调用:
groovy
people << new Person(name: 'Harry')
如果我们现在尝试再次计算 biggestSalary,我们会收到空指针异常。
为了克服这个问题,我们可以引入一个NullJob类,并将上面的语句改为:
groovy
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 的安全取消引用运算符 (?.) 运算符和 null 感知闭包通常允许 Groovy 避免创建特殊的 null 对象或 null 类。通过检查编写上述示例的更常规方式来说明这一点:
groovy
people << new Person(name:'Harry')
biggestSalary = people.collect { p -> p.job?.salary }.max()
println biggestSalary
为了让它发挥作用,这里发生了两件事。首先,max() 是 'null 感知的' ,因此 [300, null, 400].max() == 400。其次,使用 ?. 运算符,如果 salary 等于 null,或者 job 等于 null or ifpis equal to null. You don't need to code a complex nestedif ... then ... elseto avoid aNullPointerException,则像 p?.job?.salary` 这样的表达式将等于 null。
树示例
考虑以下示例,我们要计算树结构中所有值的大小、累积和和累积乘积。
我们的第一次尝试在计算方法中有特殊的逻辑来处理空值。
groovy
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() 方法中的逻辑。这些方法现在更清楚地代表了正常(现在是通用)情况的逻辑。 NullTree 中的每个方法都会返回一个表示不执行任何操作的值。
groovy
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()
运行这两个示例的结果是:
text
4
14
120
注意:空对象模式的一个细微变化是将其与单例模式结合起来。因此,我们不会在需要空对象的地方编写 new NullTree(),如上所示。相反,我们将有一个空对象实例,我们将根据需要将其放置在数据结构中。
观察者模式
观察者模式 允许一个或多个_观察者_收到关于_subject_对象的更改或事件的通知。
plantuml
!pragma layout smetana
skinparam ClassBorderColor<<Hidden>> Transparent
skinparam ClassBackgroundColor<<Hidden>> Transparent
skinparam ClassStereotypeFontColor<<Hidden>> Transparent
skinparam ClassFontSize<<Hidden>> 24
skinparam ClassFontStyle<<Hidden>> bold
skinparam shadowing<<Hidden>> false
hide <<Hidden>> circle
class "..." as ConcreteHidden
class ConcreteHidden <<Hidden>> {
}
class Observer {
+update()
}
class ConcreteObserver1 {
+update()
}
class ConcreteObserverN {
+update()
}
hide Observer fields
class Subject {
-observerCollection
+registerObserver(observer)
+unregisterObserver(observer)
+notifyObservers()
}
Observer <|-- ConcreteObserver1
Observer <|-[hidden]- ConcreteHidden
Observer <|-- ConcreteObserverN
Observer ---r---o Subject
ConcreteObserver1 .r[hidden]. ConcreteHidden
示例
这是经典模式的典型实现:
groovy
interface Observer {
void update(message)
}
class Subject {
private List observers = []
void register(observer) {
observers << observer
}
void unregister(observer) {
observers -= observer
}
void notifyAll(message) {
observers.each{ it.update(message) }
}
}
class ConcreteObserver1 implements Observer {
def messages = []
void update(message) {
messages << message
}
}
class ConcreteObserver2 implements Observer {
def messages = []
void update(message) {
messages << message.toUpperCase()
}
}
def o1a = new ConcreteObserver1()
def o1b = new ConcreteObserver1()
def o2 = new ConcreteObserver2()
def observers = [o1a, o1b, o2]
new Subject().with {
register(o1a)
register(o2)
notifyAll('one')
}
new Subject().with {
register(o1b)
register(o2)
notifyAll('two')
}
def expected = [['one'], ['two'], ['ONE', 'TWO']]
assert observers*.messages == expected
使用闭包,我们可以避免创建具体的观察者类,如下所示:
groovy
interface Observer {
void update(message)
}
class Subject {
private List observers = []
void register(Observer observer) {
observers << observer
}
void unregister(observer) {
observers -= observer
}
void notifyAll(message) {
observers.each{ it.update(message) }
}
}
def messages1a = [], messages1b = [], messages2 = []
def o2 = { messages2 << it.toUpperCase() }
new Subject().with {
register{ messages1a << it }
register(o2)
notifyAll('one')
}
new Subject().with {
register{ messages1b << it }
register(o2)
notifyAll('two')
}
def expected = [['one'], ['two'], ['ONE', 'TWO']]
assert [messages1a, messages1b, messages2] == expected
作为 Groovy 3+ 的变体,我们考虑删除 Observer 接口并使用 lambda,如下所示:
groovy
import java.util.function.Consumer
class Subject {
private List<Consumer> observers = []
void register(Consumer observer) {
observers << observer
}
void unregister(observer) {
observers -= observer
}
void notifyAll(message) {
observers.each{ it.accept(message) }
}
}
def messages1a = [], messages1b = [], messages2 = []
def o2 = { messages2 << it.toUpperCase() }
new Subject().with {
register(s -> messages1a << s)
register(s -> messages2 << s.toUpperCase())
notifyAll('one')
}
new Subject().with {
register(s -> messages1b << s)
register(s -> messages2 << s.toUpperCase())
notifyAll('two')
}
def expected = [['one'], ['two'], ['ONE', 'TWO']]
assert [messages1a, messages1b, messages2] == expected
我们现在从 Consumer 调用 accept 方法,而不是从 Observer 调用 update 方法。
@Bindable 和 @Vetoable
JDK 有一些遵循观察者模式的内置类。由于各种限制,java.util.Observer 和 java.util.Observable 类已从 JDK 9 中弃用。相反,建议您使用 java.beans 包中各种更强大的类,例如 java.beans.PropertyChangeListener。幸运的是,Groovy 有一些内置转换(groovy.beans.Bindable 和 groovy.beans.Vetoable),它们支持该包中的一些关键类。
groovy
import groovy.beans.*
import java.beans.*
class PersonBean {
@Bindable String first
@Bindable String last
@Vetoable Integer age
}
def messages = [:].withDefault{[]}
new PersonBean().with {
addPropertyChangeListener{ PropertyChangeEvent ev ->
messages[ev.propertyName] << "prop: $ev.newValue"
}
addVetoableChangeListener{ PropertyChangeEvent ev ->
def name = ev.propertyName
if (name == 'age' && ev.newValue > 40)
throw new PropertyVetoException()
messages[name] << "veto: $ev.newValue"
}
first = 'John'
age = 35
last = 'Smith'
first = 'Jane'
age = 42
}
def expected = [
first:['prop: John', 'prop: Jane'],
age:['veto: 35'],
last:['prop: Smith']
]
assert messages == expected
在这里,addPropertyChangeListener 等方法的作用与前面示例中的 registerObserver 相同。有一个 firePropertyChange 方法对应于前面示例中的 notifyAll/notifyObservers,但 Groovy 会在此处自动添加该方法,因此它在源代码中不可见。还有一个 propertyChange 方法与前面示例中的 update 方法相对应,但同样,此处不可见。
增强库模式
增强库 模式提出了一种扩展库的方法,该方法几乎可以完成您需要的所有操作,但只需要多一点。它假设您没有感兴趣的库的源代码。
示例
假设我们想要使用 Groovy 中内置的 Integer 工具(它构建在 Java 中已有的功能之上)。这些库几乎具有我们想要的所有功能,但并非全部。我们可能没有 Groovy 和 Java 库的所有源代码,因此我们不能仅仅更改库。相反,我们扩充了库。 Groovy 有多种方法可以做到这一点。一种方法是使用类别。
首先,我们将定义一个合适的类别。
groovy
class EnhancedInteger {
static boolean greaterThanAll(Integer self, Object[] others) {
greaterThanAll(self, others)
}
static boolean greaterThanAll(Integer self, others) {
others.every { self > it }
}
}
我们添加了两个方法,通过提供 greaterThanAll 方法来增强 Integer 方法。类别遵循约定,将它们定义为静态方法,并带有表示我们希望扩展的类的特殊第一个参数。 greaterThanAll(Integer self, others) 静态方法成为 greaterThanAll(other) 实例方法。
我们定义了 greaterThanAll 的两个版本。一种适用于集合、范围等。另一种适用于可变数量的 Integer 参数。
以下是如何使用该类别。
groovy
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])
}
正如您所看到的,使用这种技术,您可以有效地丰富原始类,而无需访问其源代码。此外,您可以在系统的不同部分应用不同的丰富功能,也可以根据需要使用未丰富的对象。
代理模式
代理模式 允许一个对象充当其他对象的假装替代品。一般来说,无论谁使用代理,都不会意识到他们没有使用真实的东西。当真实对象难以创建或使用时,该模式非常有用:它可能存在于网络连接上,或者是内存中的大型对象,或者是文件、数据库或其他昂贵或无法复制的资源。
示例
代理模式的一种常见用途是与不同 JVM 中的远程对象进行通信。以下是用于创建通过套接字与服务器对象通信的代理的客户端代码以及示例用法:
groovy
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
您的服务器代码可能如下所示(首先启动它):
groovy
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)
}
}
}
单例模式
单例模式 用于确保只创建特定类的一个对象。当只需要一个对象来协调整个系统的操作时,这会很有用。也许是为了提高效率,创建大量相同的对象会很浪费,可能是因为需要需要单点控制的特定算法,或者可能是当对象用于与不可共享资源交互时。
单例模式的缺点包括:
-
它可以减少重复使用。例如,如果您想将继承与单例一起使用,就会出现问题。如果
SingletonB扩展SingletonA,则每个类是否应该(最多)有一个实例,或者从其中一个类创建对象是否应该禁止从另一个类创建对象。另外,如果您决定两个类都可以有一个实例,那么如何重写静态的getInstance()方法? -
由于静态方法,一般来说测试单例也很困难,但如果需要,Groovy 可以支持这一点。
示例:经典 Java 单例
假设我们希望创建一个用于收集选票的类。因为获得正确的票数可能非常重要,所以我们决定使用单例模式。永远只有一个 VoteCollector 对象,因此它使我们更容易推断该对象的创建和使用。
groovy
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也是私有的,因此一旦设置就无法更改 -
此时我们还没有使投票更新成为线程安全的(它不会添加到此示例中)
-
投票收集器实例不是延迟创建的(如果我们从不引用该类,则不会创建该实例;但是,一旦我们引用该类,即使最初不需要该实例,也会立即创建该实例)
我们可以在一些脚本代码中使用这个单例类,如下所示:
groovy
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次。第二次用法甚至是在不同的线程中(但不要在使用新类加载器的场景中尝试此操作)。
运行此脚本会产生(您的哈希码值会有所不同):
text
Collector:15959960, Votes:0
Collector:15959960, Votes:1
Collector:15959960, Votes:2
此模式的变体:
-
为了支持延迟加载和多线程,我们可以将
synchronized关键字与getInstance()方法一起使用。这会影响性能,但会起作用。 -
我们可以考虑涉及双重检查锁定和
volatile关键字的变体,但请参阅此方法的局限性 此处。
示例:通过元编程进行单例
Groovy 的元编程功能允许以更基本的方式实现诸如单例模式之类的概念。此示例说明了使用 Groovy 的元编程功能来实现单例模式的简单方法,但不一定是最有效的方法。
假设我们想要跟踪计算器执行的计算总数。一种方法是对计算器类使用单例,并在类中保留一个带有计数的变量。
首先我们定义一些基类。执行计算并记录其执行的此类计算次数的 Calculator 类,以及充当计算器外观的 Client 类。
groovy
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 系统注册这个 MetaClass:
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 类的实例。客户端类将尝试创建计算器的新实例,但始终会获得单例。
groovy
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"
以下是运行此脚本的结果(您的哈希码值可能会有所不同):
text
Client: 7306473, Calc: 24230857, Total Calculations: 1
Client: 31436753, Calc: 24230857, Total Calculations: 2
Guice 示例
我们还可以使用 Guice 来实现单例模式。
再次考虑计算器的例子。
Guice是一个面向Java的框架,支持面向接口的设计。因此我们首先创建一个 Calculator 接口。然后,我们可以创建 CalculatorImpl 实现和脚本将与之交互的 Client 对象。本示例并不严格需要 Client 类,但它允许我们表明非单例实例是默认的。这是代码:
groovy
@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 来创建实例。到目前为止,没有显示任何内容会阻止您使用 new CalculatorImpl() 手动创建计算器的另一个实例,这当然会违反所需的单例行为。
在其他场景中(尽管可能不是在大型系统中),我们可以选择使用注释来表达依赖关系,例如以下示例所示:
groovy
@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 注释。
运行时,上面的示例(使用任一方法)会产生(您的哈希码值会有所不同):
text
Client: 8897128, Calc: 17431955, Total Calculations: 1
Client: 21145613, Calc: 17431955, Total Calculations: 2
您可以看到,每当我们请求实例时,我们都会获得一个新的客户端对象,但它被注入了相同的计算器对象。
Spring 示例
我们可以使用 Spring 再次执行计算器示例,如下所示:
groovy
@Grapes([@Grab('org.springframework:spring-core:5.2.8.RELEASE'), @Grab('org.springframework:spring-beans:5.2.8.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() }
}
// Here we 'wire' up our dependencies through the API. Alternatively,
// we could use XML-based configuration or the 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"
这是结果(您的哈希码值会有所不同):
text
Client: 29418586, Calc: 10580099, Total Calculations: 1
Client: 14800362, Calc: 10580099, Total Calculations: 2
更多信息
状态模式
状态模式提供了一种结构化方法来划分复杂系统中的行为。系统的整体行为被划分为明确定义的状态。通常,每个状态由一个类实现。首先可以通过了解系统的当前状态来确定整个系统的行为;其次,通过理解在该状态下可能的行为(体现在与该状态对应的类的方法中)。
示例
这是一个例子:
groovy
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()
这是输出:
text
offline
error: not connected
connected
"Hello" sent
error: already connected
message received
offline
像 Groovy 这样的动态语言的优点之一是我们可以根据我们的特定需求以许多不同的方式来表达这个例子。下面显示了此示例的一些潜在变化。
变体 1:利用面向接口的设计
我们可以采取的一种方法是利用面向接口的设计。为此,我们可以引入以下接口:
groovy
interface State {
def connect()
def disconnect()
def send_message(message)
def receive_message()
}
然后我们的 Client、Online 和"Offline"类可以修改以实现该接口,例如:
groovy
class Client implements State {
// ... as before ...
}
class Online implements State {
// ... as before ...
}
class Offline implements State {
// ... as before ...
}
您可能会问:我们不是刚刚引入了额外的样板代码吗?我们不能依靠鸭子类型来实现这一点吗?答案是"是"和"否"。我们可以摆脱鸭子类型,但状态模式的关键意图之一是划分复杂性。如果我们知道 client 类和每个 state 类都满足一个接口,那么我们就围绕复杂性设置了一些关键边界。我们可以孤立地查看任何状态类,并了解该状态可能的行为范围。
我们不必为此使用接口,但它有助于表达这种特定分区风格的意图,并且有助于减少单元测试的大小(我们必须有额外的测试来用对面向接口设计支持较少的语言表达此意图)。
变体 2:提取状态模式逻辑
或者,或者与其他变体相结合,我们可能决定将一些状态模式逻辑提取到辅助类中。例如,我们可以在状态模式 package/jar/script 中定义以下类:
groovy
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))
}
}
这都是非常通用的,可以在任何我们想要引入状态模式的地方使用。现在我们的代码如下所示:
groovy
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()
您可以在这里看到 startFrom 和 transitionTo 方法开始为我们的示例代码提供 DSL 的感觉。
变体 3:启用 DSL
或者,或者与其他变体相结合,我们可能决定在此示例中完全采用领域特定语言(DSL)方法。
我们可以定义以下通用辅助函数(首先讨论此处):
groovy
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"
}
}
groovy
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
}
}
现在我们可以像这样定义和测试我们的状态机:
groovy
class StatePatternDslTest {
private fsm
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')
}
}
此示例与其他示例并不完全相同。它不使用预定义的 Online 和 Offline 类。相反,它根据需要动态定义整个状态机。有关此样式的更详细示例,请参阅 先前的参考。
策略模式
策略模式 允许您从其使用中抽象出特定的算法。这使您可以轻松交换正在使用的算法,而无需更改调用代码。该模式的一般形式是:
plantuml
!pragma layout smetana
hide fields
hide <<Hidden>> circle
skinparam ClassBorderColor<<Hidden>> Transparent
skinparam ClassBackgroundColor<<Hidden>> Transparent
skinparam ClassStereotypeFontColor<<Hidden>> Transparent
skinparam ClassFontSize<<Hidden>> 24
skinparam ClassFontStyle<<Hidden>> bold
skinparam shadowing<<Hidden>> false
class Context {
+Strategy getStrategy()
}
class Strategy {
+algorithmMethod()
}
class ConcreteStrategy1 {
+algorithmMethod()
}
class ConcreteStrategy2 {
+algorithmMethod()
}
class "..." as ConcreteHidden
class ConcreteHidden <<Hidden>> {
}
class ConcreteStrategyN {
+algorithmMethod()
}
Context o---r--- Strategy
Strategy <|-- ConcreteStrategy1
Strategy <|-- ConcreteStrategy2
Strategy <|-[hidden]- ConcreteHidden
Strategy <|-- ConcreteStrategyN
在 Groovy 中,由于它能够使用匿名方法(我们宽松地称为 Closures)将代码视为第一类对象,因此大大减少了对策略模式的需求。您可以简单地将算法放置在闭包中。
使用传统类层次结构的示例
首先让我们看一下传统的策略模式封装方式。
groovy
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(不要尝试使用负数 - 是的,我们可以解决这个问题,但这只会使示例更长)。然后我们使用普通的 多态 来调用算法。
使用闭包的示例
以下是使用闭包实现相同目标的 Groovier 方法:
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])
}
}
使用 lambda 的示例
对于 Groovy 3+,我们可以利用 lambda 语法:
groovy
interface Calc {
def execute(n, m)
}
List<Calc> 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])
}
}
或者我们可以使用内置的 JDK BiFunction 类:
groovy
import java.util.function.BiFunction
List<BiFunction<Integer, Integer, Integer>> 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])
}
}
模板方法模式
模板方法模式抽象了几种算法的细节。算法的通用部分包含在基类中。特定的实现细节在子类中捕获。所涉及的类的通用模式如下所示:
plantuml
!pragma layout smetana
skinparam nodesep 100
class AbstractClass {
+algorithm1()
+algorithm2()
{abstract} +primitiveOperationA()
{abstract} +primitiveOperationB()
}
class ConcreteClass1 {
+primitiveOperationA()
+primitiveOperationB()
}
class ConcreteClass2 {
+primitiveOperationA()
+primitiveOperationB()
}
object templateMethodUser
hide fields
templateMethodUser ......r......> "<<use>>" AbstractClass
AbstractClass <|-- ConcreteClass1
AbstractClass <|-- ConcreteClass2
使用传统类的示例
在此示例中,基本 Accumulator 类捕获了累加算法的本质。子类 Sum 和 Product 提供了使用通用累加算法的特定定制方法。
groovy
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 }
}
assert 10 == new Sum().accumulate([1,2,3,4])
assert 24 == new Product().accumulate([1,2,3,4])
使用简化策略的示例
在这种特殊情况下,您可以使用 Groovy 的注入方法通过闭包实现类似的结果:
groovy
Closure addAll = { total, item -> total += item }
def accumulated = [1, 2, 3, 4].inject(0, addAll)
assert accumulated == 10
由于鸭子类型,这也适用于支持 add(Groovy 中的 plus())方法的其他对象,例如:
groovy
accumulated = [ "1", "2", "3", "4" ].inject("", addAll)
assert accumulated == "1234"
我们还可以按如下方式进行乘法情况(重写为一行):
groovy
assert 24 == [1, 2, 3, 4].inject(1) { total, item -> total *= item }
以这种方式使用闭包看起来像 策略模式,但是如果我们意识到 Groovy 的 inject 方法是我们模板方法算法的通用部分,那么闭包就成为模板方法模式的定制部分。
对于 Groovy 3+,我们可以使用 lambda 语法作为闭包语法的替代:
groovy
assert 10 == [1, 2, 3, 4].stream().reduce(0, (l, r) -> l + r)
assert 24 == [1, 2, 3, 4].stream().reduce(1, (l, r) -> l * r)
assert '1234' == ['1', '2', '3', '4'].stream().reduce('', (l, r) -> l + r)
这里流 api 的 reduce 方法是我们模板方法的算法的通用部分,而 lambda 是模板方法模式的定制部分。
访问者模式
访客模式 是众所周知但不常用的模式之一。也许这是因为一开始看起来有点复杂。但是一旦您熟悉了它,它就会成为改进代码的强大方法,并且正如我们将看到的,Groovy 提供了降低复杂性的方法,因此没有理由不考虑使用此模式。
该模式的目标是将算法与对象结构分开。这种分离的实际结果是能够向现有对象结构添加新操作,而无需修改这些结构。
简单示例
此示例考虑如何计算形状(或形状集合)的边界。我们的第一次尝试使用传统的访客模式。我们很快就会看到一种更 Groovy 的方式来做到这一点。
groovy
abstract class Shape { }
@ToString(includeNames=true)
class Rectangle extends Shape {
def x, y, w, h
Rectangle(x, y, w, h) {
this.x = x; this.y = y; this.w = w; this.h = h
}
def union(rect) {
if (!rect) return this
def minx = [rect.x, x].min()
def maxx = [rect.x + rect.w, x + w].max()
def miny = [rect.y, y].min()
def maxy = [rect.y + rect.h, y + h].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.x2].min(),
[line.y1, line.y2].min(),
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
assert bounding_box.toString() == 'Rectangle(x:60, y:5, w:50, h:70)'
这需要相当多的代码,但现在的想法是,我们可以通过添加新的访问者来添加进一步的算法,而我们的形状类保持不变,例如我们可以添加总区域访客或碰撞检测访客。
我们可以通过使用 Groovy 闭包来提高代码的清晰度(并将其大小缩小到一半左右),如下所示:
groovy
abstract class Shape {
def accept(Closure yield) { yield(this) }
}
@ToString(includeNames=true)
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 + rect.w, x + w ].max()
def miny = [ rect.y, y ].min()
def maxy = [ rect.y + rect.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) }
assert bounds.toString() == 'Rectangle(x:60, y:5, w:50, h:70)'
或者,按如下方式使用 lambda:
groovy
/* ... same as with Closures ... */
}
class Group {
def shapes = []
def leftShift(shape) { shapes += shape }
def accept(Function<Shape, Shape> yield) {
shapes.stream().forEach(s -> s.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(s -> { bounds = s.bounds().union(bounds) })
assert bounds.toString() == 'Rectangle(x:60, y:5, w:50, h:70)'
高级示例
让我们考虑另一个例子来说明有关此模式的更多要点。
groovy
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:
groovy
NodeType1 root = new NodeType1()
root.children = new Visitable[]{new NodeType1(), new NodeType2()}
def counter = new NodeType1Counter()
root.accept(counter)
assert counter.count == 2
然后,我们有一个 NodeType1 对象作为根,其中一个子对象也是一个 NodeType1 实例。另一个子实例是 NodeType2 实例。这意味着在此处使用 NodeType1Counter 应在最后一条语句验证时计算 2 个 NodeType1 对象。
何时使用该模式
此示例说明了访问者模式的一些优点。例如,虽然我们的访问者有状态(NodeType1 对象的计数),但对象树本身并没有改变。类似地,如果我们希望有一个访问者对所有节点类型进行计数,或者对使用了多少种不同类型进行计数,或者使用节点类型特有的方法收集信息,则只需编写访问者即可。
如果添加新类型会发生什么?
在这种情况下,我们可能有相当多的工作要做。我们可能必须更改 Visitor 接口以接受新类型,并根据该接口更改更改可能的大多数现有访问者,并且我们必须自己编写新类型。更好的方法是编写访问者的默认实现,所有具体访问者都将扩展它。我们很快就会看到这种方法的使用。
如果想使用不同的迭代模式怎么办?
那么你就有问题了。由于节点描述了如何迭代,因此您无法影响并在某个点停止迭代或更改顺序。所以也许我们应该稍微改变一下:
groovy
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 现在能够捕获新类型,我们可以通过不委托给 super 来停止迭代。当然现在最大的缺点是不再迭代,但你无法获得所有好处。
让它更 Groovy
现在的问题是如何让它更加 Groovy 一点。你不觉得这个visitor.visit(this)很奇怪吗?为什么它在那里?答案是模拟双重调度。在Java中,使用编译时类型,因此对于visitor.visit(children[i]),编译器将无法找到正确的方法,因为Visitor不包含方法visit(Visitable)。即使可以,我们也希望使用 NodeType1 或 NodeType2 来访问更特殊的方法。
现在 Groovy 不使用静态类型,Groovy 使用运行时类型。这意味着我们可以毫无问题地使用 visitor.visit(children[i])。既然我们最小化了accept方法来只执行双重调度部分,并且Groovy的运行时类型系统已经涵盖了这一点,那么我们是否需要accept方法?不是真的,但我们可以做得更多。我们的缺点是不知道如何处理未知的树元素。为此,我们必须_扩展_接口 Visitor,从而导致 DefaultVisitor 发生更改,然后我们的任务是提供有用的默认值,例如迭代节点或根本不执行任何操作。现在,使用 Groovy,我们可以通过添加一个不执行任何操作的 visit(Visitable) 方法来捕获这种情况。顺便说一句,这在 Java 中也是一样的。
但不要让我们停在这里。我们需要 Visitor 接口吗?如果我们没有accept方法,那么我们根本不需要Visitor接口。所以新的代码是:
groovy
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 接口,让它返回我们接下来要访问的子项。这为我们提供了通用的迭代方法。
groovy
class DefaultVisitor {
void visit(Visitable v) {
doIteration(v)
}
void doIteration(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 也进行了更改,以确保任何节点都能够返回子节点(即使为空)。我们不必更改 NodeType1 和 NodeType2 类,因为 Children 字段的定义方式已经使它们成为一个属性,这意味着 Groovy 非常好地为我们生成了 get 方法。现在真正有趣的部分是 NodeType1Counter,它很有趣,因为我们没有改变它。 super.visit(n1) 现在将调用 visit(Visitable),visit(Visitable) 将调用 doIteration,这将开始下一个迭代级别。所以没有改变。但如果 visit(it) 类型为 NodeType1,则 visit(NodeType1) 将调用 visit(NodeType1)。事实上,我们不需要 doIteration 方法,我们也可以在 visit(Visitable) 中这样做,但这个变体有一些好处。它允许我们编写一个新的 Visitor 来覆盖错误情况下的访问(Visitable),这当然意味着我们不能执行 super.visit(n1),而是执行 doIteration(n1)。
总结
最终,我们减少了约 40% 的代码,获得了健壮且稳定的架构,并且我们完全从 Visitable 中删除了 Visitor。要在 Java 中实现相同的目的,您可能需要求助于反射。
访问者模式有时被描述为不适合极限编程技术,因为您需要始终对如此多的类进行更改。根据我们的设计,如果添加新类型,我们不需要更改任何内容。因此,该模式非常适合使用 Groovy 时的敏捷方法。
访问者模式有多种变体,例如 非循环访问者模式,它们试图解决添加具有特殊访问者的新节点类型的问题。这些访问者的实现有自己的代码味道,例如使用强制转换、过度使用 instanceof 以及其他技巧。更重要的是,这些方法试图解决的问题不会出现在 Groovy 版本中。我们建议避免这种模式的变体。
最后,如果不是很明显,NodeType1Counter 也可以用 Java 实现。 Groovy 将识别访问方法并根据需要调用它们,因为 DefaultVisitor 仍然是 Groovy 并且可以完成所有的魔力。