这是一个由两部分组成的文章的第二部分,该文章说明了计算机语言的表达方式如何通过允许您将更多的精力集中在本质而不是仪式上来帮助紧急设计。 意图和结果之间的鸿沟是数十年历史悠久的语言(包括Java™语言)的特征,为解决问题增加了不必要的仪式。 更具表现力的语言使代码查找更容易,因为代码包含的噪音更少。 这种表现力是Groovy和Scala等现代语言的标志。 较老但更具表现力的语言,例如Ruby,其中JRuby是JVM的变体; 或重新构想的旧语言,例如Clojure,JVM上的现代Lisp(请参阅参考资料 )。 在本文中,我将继续我在第1部分中开始的演示-以更具表现力的语言实现“ 设计模式”书中的传统“四人一组”模式。
装饰图案
《四人帮》一书将装饰器模式定义为:
动态地将附加职责附加到对象。 装饰器为子类提供了灵活的替代方案,以扩展功能。
如果您曾经使用过java.io.*
包,那么您将非常清楚Decorator模式。 显然,I / O库的设计人员阅读了《四人帮》一书的“装饰器”部分,并将其真正铭记在心! 首先,我将在Groovy中展示Decorator模式的传统实现,然后在后续示例中使其更加动态。
传统装饰
清单1显示了一个Logger
类,以及几个用Groovy实现的装饰器( TimeStampingLogger
和UpperLogger
):
清单1.记录器和两个装饰器
class Logger {
def log(String message) {
println message
}
}
class TimeStampingLogger extends Logger {
private Logger logger
TimeStampingLogger(logger) {
this.logger = logger
}
def log(String message) {
def now = Calendar.instance
logger.log("$now.time: $message")
}
}
class UpperLogger extends Logger {
private Logger logger
UpperLogger(logger) {
this.logger = logger
}
def log(String message) {
logger.log(message.toUpperCase())
}
}
Logger
是一个简单的记录器,可将日志消息写入控制台。 TimeStampingLogger
通过修饰添加时间戳,而UpperLogger
将日志消息更改为大写。 要使用这两个装饰器中的任何一个,都可以用适当的装饰器包装Logger
实例,如清单2所示:
清单2.使用装饰器包装记录器
def logger = new UpperLogger(
new TimeStampingLogger(
new Logger()))
logger.log("Groovy Rocks")
清单2的输出显示了一条带有大写时间戳记的日志消息:
Tue May 22 07:13:50 EST 2007: GROOVY ROCKS
到目前为止,Groovy实现对于此装饰器来说只是很不寻常的事情。 但是我可以做一个装饰器,而无需添加基于类的方法的额外结构。
装饰到位
“四人帮”一书中的传统设计模式假定解决每个问题都需要建立更多的类。 但是,JVM上的现代语言还具有其他功能,例如开放类 ,它使您可以重新打开现有类并向其添加新方法而无需子类化。 当您需要更改需要特定类的部分基础结构(例如,集合API)使用的类的行为时,这特别方便。 您可以修改现有类,将其作为参数传递,并利用API的优势,而无需基础API声明抽象类或接口。 开放类还允许您“就地”进行修改,而无需子类化。
但是,更改整个类的定义听起来很可怕:您可能不希望在整个宇宙中进行广泛的更改。 幸运的是,Groovy和Ruby都允许您向类的单个实例添加新方法。 换句话说,您可以将新方法仅添加到Logger
的实例,而不会影响该方法的所有其他实例。 清单3显示了在Groovy中使用ExpandoMetaClass
覆盖Logger
的单个实例上的log()
方法:
清单3.在Logger
实例上重写log()
方法
def logger = new Logger()
logger.metaClass.log = { String m ->
println m.toUpperCase()
}
logger.log "this log message brought to you in upper case"
一旦了解了该机制的工作原理,读取此代码比读取使用其他类的相应代码要简单得多。 所有相关的修饰代码都显示在一个位置,而不是分散在多个文件中(因为使用Java语言,每个公共类必须驻留在其自己的文件中)。
在Ruby中,使用相同的功能(使用称为singleton方法 (这是一个令人困惑的名称,因为singleton非常重载))或eigenclass的Ruby功能。 清单4中显示了用JRuby实现的相同代码:
清单4.使用Ruby的特征类进行就地装饰
class Logger
def log(msg)
puts msg
end
end
l = Logger.new
def l.log m
puts m.upcase
end
l.log "this log message brought to you in upper case"
Ruby版本没有使用额外的工具,例如ExpandoMeta Class
。 在Ruby中,您可以通过将变量名称放在方法声明的开头来为特定实例内联定义方法。 Ruby具有极大的语法灵活性,对何时何地可以定义方法施加更少的规则。
该功能也适用于内置Java类。 例如,应该使用first()
和last()
方法定义ArrayList
类,但是事实并非如此。 但是,在Groovy中添加这些方法很容易,如清单5所示:
清单5. Groovy为ArrayList
添加了first()
和last()
方法
ArrayList.metaClass.getFirst {
delegate.size > 0 ? get(0) : null
}
ArrayList.metaClass.getLast {
delegate.size > 0 ? get(delegate.size - 1) : null
}
ArrayList l = new ArrayList()
l << 1 << 2 << 3
println l.first
println l.last
ArrayList emptyList = new ArrayList()
println emptyList.first
println emptyList.last
ExpandoMetaClass
允许您在类上定义新的属性(使用熟悉的Java get / set命名模式)。 在类上定义新属性后,就可以像正常属性一样调用它们。
您可以使用现有的JDK类在JRuby中执行相同的操作,如清单6所示:
清单6.使用JRuby将方法添加到ArrayList
require 'java'
include_class 'java.util.ArrayList'
class ArrayList
def first
size != 0 ? get(0) : nil
end
def last
size != 0 ? get(size - 1) : nil
end
end
list = ArrayList.new
l << 1 << 2 << 3
puts list.first
puts list.last
empty_list = ArrayList.new
puts empty_list.first
puts empty_list.last
不要陷入认为解决每个问题都需要更多类的陷阱。 元编程通常可以为问题提供更清晰的解决方案。
带调用挂钩的装饰器
有时,您需要的装饰不仅涉及几个课程。 例如,您可能想用事务性控件修饰所有数据库操作。 为每种情况创建一个简单的传统装饰器太麻烦了,并且会在代码中添加太多语法,以致于很难确定目标目标。
考虑清单7所示的装饰器,该装饰器是用Groovy实现的:
清单7. Groovy中的GenericLowerDecorator
class GenericLowerDecorator {
private delegate
GenericLowerDecorator(delegate) {
this.delegate = delegate
}
def invokeMethod(String name, args) {
def newargs = args.collect{ arg ->
if (arg instanceof String) return arg.toLowerCase()
else return arg
}
delegate.invokeMethod(name, newargs)
}
}
GenericLowerDecorator
类充当通用装饰器,将所有基于字符串的参数强制转换为小写。 它是通过使用hook方法来实现的。 调用此装饰器时,会将其包装在任何实例周围。 invokeMethod()
方法拦截对此类的所有方法调用,从而使您可以执行所需的任何操作。 在这种情况下,我将拦截每个方法调用并遍历所有方法参数。 如果任何参数的类型均为String
,则将其小写版本添加到新的参数列表中,并保留其他参数不变。 在hook方法的结尾,我使用新的参数列表在装饰对象上调用原始方法调用。 此修饰器将所有字符串参数都转换为小写字母,而不管方法或其参数如何。 清单8显示了其用法的示例,包装了清单1中的一个记录器:
清单8.在Logger
上使用GenericLowerDecorator
logger = new GenericLowerDecorator(
new TimeStampingLogger(
new Logger()))
logger.log('IMPORTANT Message')
用此装饰器调用的任何方法仅使用小写字符串:
Tue May 22 07:27:18 EST 2007: important message
请注意,时间戳记不是小写的,而String
参数是小写的。 用Java语言可以实现,但是非常困难。 实际上,使用方面(例如,通过AspectJ)是用Java语言实现这种效果的唯一方法(请参阅参考资料 )。 要获得这种类型的装饰器,必须使用自己的编译器切换到另一种语言,并为Java代码设置后处理。 尽管并非不可能,但此过程非常繁琐,您再也不会打扰。
适配器模式
《四人帮》一书将Adapter模式描述为:
将类的接口转换为客户端期望的另一个接口。 适配器使类可以协同工作,否则由于接口不兼容而无法实现。
如果您曾经在Swing中使用过事件处理程序,那么您将对Adapter模式有深入的了解。 它用于围绕事件处理接口创建适配器类,该接口包含多个方法,因此您无需创建自己的类,实现该接口并包含许多空方法。 Swing适配器允许您对适配器进行子类化,而只是覆盖事件所需的方法。
适应Groovy
但是,最终,适配器模式尝试回答以下问题:“我能使这个方形钉适合此圆Kong吗?” 这就是我要解决的问题,它有两个不同的实现,每个实现都突出了语言的表现力。 第一个实现使用Groovy。 清单9中显示了这三个类和一个感兴趣的接口:
清单9.方钉和圆Kong
interface RoundThing {
def getRadius()
}
class SquarePeg {
def width
}
class RoundPeg {
def radius
}
class RoundHole {
def radius
def pegFits(peg) {
peg.radius <= radius
}
String toString() { "RoundHole with radius $radius" }
}
传统的适配器实现将创建一个SquarePegAdaptor
类,包装的方挂和实施getRadius()
由预期的方法pegFits()
的方法RoundHole
。 但是,Groovy允许我绕过附加类的附加结构,直接内联定义适配器,如清单10所示:
清单10.测试内联适配器
@Test void pegs_and_holes() {
def adapter = { p ->
[getRadius:{Math.sqrt(
((p.width/2) ** 2)*2)}] as RoundThing
}
def hole = new RoundHole(radius:4.0)
(4..7).each { w ->
def peg = new SquarePeg(width:w)
if (w < 6)
assertTrue hole.pegFits(adapter(peg))
else
assertFalse hole.pegFits(adapter(peg))
}
}
适配器定义看起来有些奇怪,但是封装了许多功能。 我将adaptor
定义为代码块(在Groovy中由{
分隔)。 在代码块内部,我创建了一个哈希,其中的键是属性的名称( getRadius()
),值是实现我所需的适配器功能的代码块。 Groovy中的as
运算符可以执行魔术操作。 当我在代码块上使用as
运算符时,Groovy将创建一个实现RoundThing
接口的新类。 该类上的方法调用在哈希中执行查找,将方法名称与键值进行匹配,并执行相应的代码块。 最终结果是实现了RoundThing
接口所需功能的轻量级适配器类。
尽管类级别的最终实现看起来与传统方法相同,但是代码(一旦您知道Groovy)就更易于阅读和理解。 Groovy允许您针对这种情况在接口周围创建轻量级包装类。
JRuby中的适配器模式
如果您根本不想为适配器创建额外的类怎么办? Groovy和Ruby都支持开放类,使您可以将所需的方法直接添加到相关的类中。 清单11中显示了Ruby中的方钉和圆Kong的实现(通过JRuby):
清单11. Ruby中的开放类适配器
class SquarePeg
attr_reader :width
def initialize(width)
@width = width
end
end
class SquarePeg
def radius
Math.sqrt(((@width/2) ** 2) * 2 )
end
end
class RoundPeg
attr_reader :radius
def initialize(radius)
@radius = radius
end
def width
@radius * @radius
end
end
class RoundHole
attr_reader :radius
def initialize(r)
@radius = r
end
def peg_fits?( peg )
peg.radius <= radius
end
end
清单11中 SquarePeg
类的第二个定义不是一个错误:Ruby的开放类语法看起来像是常规的类定义。 当您使用类名时,Ruby会检查它是否已经使用相同的名称从类路径中加载了一个类,如果已加载,则第二次出现会重新打开该类。 当然,在这种情况下,我可以直接将radius()
方法添加到该类中,但是我假设原始SquarePeg
类早于该代码。 清单12显示了开放类适配器的单元测试:
清单12.测试开放类适配器
def test_open_class_pegs
hole = RoundHole.new( 4.0 )
4.upto(7) do |i|
peg = SquarePeg.new(i.to_f)
if (i < 6)
assert hole.peg_fits?(peg)
else
assert ! hole.peg_fits?(peg)
end
end
end
在这种情况下,我可以直接在SquarePeg
类上调用radius
方法,因为它现在具有radius
方法。 通过开放类添加方法完全不需要手工编写或自动生成的单独适配器类。 但是,此代码确实存在潜在的问题:如果SquarePeg
类已经具有与圆Kong无关的radius
方法,该怎么办? 使用开放类将覆盖原始类,从而导致不良行为。
这就是真正表达语言的力量的全部体现。 考虑清单13中的Ruby代码:
清单13.接口切换
class SquarePeg
include InterfaceSwitching
def radius
@width
end
def_interface :square, :radius
def radius
Math.sqrt(((@width/2) ** 2) * 2)
end
def_interface :holes, :radius
def initialize(width)
set_interface :square
@width = width
end
end
实际上,用Java语言或Groovy编写此代码是不可能的。 注意,我已经定义了两个名为radius
方法。 在Groovy中,编译器不会编译此代码。 但是,Ruby(因此是JRuby)是一种解释型语言,允许您在解释时执行代码。 您会听到一些Rubyists将Ruby中的构造称为“一流公民”,这意味着该语言的所有部分始终可用。 这里的魔力在于(类似关键字)的def_interface
方法调用。 这是在Class
类上定义的元编程方法,该方法在解释时执行。 此代码允许您为方法定义特定的接口,从而仅允许该方法存在于特定范围内。 范围由with_interface
方法调用定义,如清单14所示:
清单14.测试接口切换
def test_pegs_switching
hole = RoundHole.new( 4.0 )
4.upto(7) do |i|
peg = SquarePeg.new(i)
peg.with_interface(:holes) do
if (i < 6)
assert hole.peg_fits?(peg)
else
assert ! hole.peg_fits?(peg)
end
end
end
end
在with_interface
块的范围内,使用该接口名称定义的radius
方法存在并且可以调用。 清单15中使该工作完成的代码非常小(但是有些密集)。 这里显示的是上下文。 其中大多数是半高级Ruby元编程,因此我将不对其进行详细讨论。
清单15.接口切换魔术
class Class
def def_interface(interface, *syms)
@__interface__ = {}
a = (@__interface__[interface] = [])
syms.each do |s|
a << s unless a.include? s
alias_method "__#{s}_#{interface}__".intern, s
remove_method s
end
end
end
module InterfaceSwitching
def set_interface(interface)
unless self.class.instance_eval{ @__interface__[interface] }
raise "Interface for #{self.inspect} not understood."
end
i_hash = self.class.instance_eval "@__interface__[interface]"
i_hash.each do |meth|
class << self; self end.class_eval <<-EOF
def #{meth}(*args, &block)
send(:__#{meth}_#{interface}__, *args, &block)
end
EOF
end
@__interface__ = interface
end
def with_interface(interface)
oldinterface = @__interface__
set_interface(interface)
begin
yield self
ensure
set_interface(oldinterface)
end
end
end
清单15中有趣的代码出现在开放类Class
定义的末尾,该命名方法被赋予另一个名称(基于接口),然后以编程方式从该类中删除。 更多有趣的代码出现在InterfaceSwitching
混入中: set_interface
方法针对在with_interface
方法中创建的块的范围重新定义了原始(重命名)方法。 Ruby的finally
块版本是finally
的ensure
块。
本练习的重点不一定是深入研究Ruby中的voodoo元编程,而是演示高表达语言中的可能性。 解释型语言始终比编译型语言具有优势,因为它们可以在编译型语言无法执行的时间执行代码。 实际上,Groovy已经引入了一种称为AST Transformations的编译时元编程机制,通过该机制,您可以编写代码以与编译器进行交互(请参阅参考资料 )。
摘要
那么,这一切证明了什么呢? 在语言中,表达等于力量。 你不会看到许多在Java语言中这些技术,即使他们是通过诸如使用了Javassist工具方面字节码生成技术上是可行的(参见相关主题 )。 但是,使用这些机制解决问题非常麻烦,没有人会打扰。 这种态度也会影响惯用模式。 即使您可以看到特定于您的应用程序的模式,但是如果很难收集它们的方式,您也不会打扰,从而在项目上积累了不必要的技术负担。 表达能力在计算机语言中很重要……很多!
翻译自: https://www.ibm.com/developerworks/java/library/j-eaed8/index.html