级别: 中级 Neal Ford, 软件架构师/Meme Wrangler, ThoughtWorks Inc.
2009 年 10 月 27 日
发现和积累惯用模式的能力对于紧急设计至关重要。对于设计而言同样十分重要的是代码的表达性。在本系列文章的第 2 部分中,Neal Ford 将继续讨论表达性和模式的交集,通过惯用模式和正式设计模式阐释这些概念。他用动态语言为 JVM 重构了一些经典的四人组(Gang of Four)模式,以说明表达性更好的语言如何使您看到被透明度不佳的语言遮挡的设计元素。
 |
关于本系列
本 系列 旨在从全新的视角来介绍经常讨论但是又难以理解的软件架构和设计概念。通过具体示例,Neal Ford 将帮助您在演化架构 和紧急设计 的灵活实践中打下坚实的基础。通过将重要的架构和设计决定推迟到最后责任时刻,您可以防止不必要的复杂度降低软件项目的质量。
|
|
本文是本系列文章的第 2 部分,旨在演示计算机语言的表达性(允许您专注于本质,而不是形式)对于紧急设计的重要作用。意图(intent)与结果(result)之间的分歧对于许多年代久远的语言(包括 Java™ 语言)来说都是一个通病,从而为问题解决工作添加了不必要的形式。表达性更好的语言可以帮助开发人员更加轻松地发现惯用模式,因为代码中包含的无用信息更少。表达性是 Groovy 和 Scala 等现代语言的特征;年代久远但表达性较好的语言包括 Ruby(其中,JRuby 是一种 JVM 变体);其他表达性较好的语言还包括经过翻新的 Clojure,以及基于 JVM 的现代 Lisp(参见 参考资料)。在本文中,我将继续 第 1 部分 中的演示 — 使用表达性更好的语言实现设计模式 一书中的传统四人组模式。
修饰符模式
四人组的书籍将修饰符模式定义为:
将额外的责任动态赋予某个对象。修饰符提供了另外一种灵活的用于扩展功能的继承方法。
如果您曾经使用过 java.io.* 包,则应该对修饰符模式有所了解。显然,I/O 库的设计者们阅读了四人组书籍的修饰符部分,并领悟了其核心意义!首先,我将演示修饰符模式在 Groovy 中的传统实现,然后再在后续示例中提高它的动态性。
传统的修饰符
清单 1 显示了一个 Logger 类,以及与该类相关的一些修饰符(TimeStampingLogger 和 UpperLogger),所有代码均在 Groovy 中实现:
清单 1. Logger 和两个修饰符
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 显示如何使用 ExpandoMetaClass 在 Groovy 中重写某个 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 method(这是一个令人疑惑的名称,因为 singleton 代表着负载过重)或者 eigenclass。在 JRuby 中实现的代码如清单 4 所示:
清单 4. 使用 Ruby 的 eigenclass 执行就位修饰
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 命名模式)。为类定义了新的属性之后,可以将它们像普通属性一样调用。
您可以像在 JRuby 中一样使用已有的 JDK 类实现相同的目的,如清单 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 类型,则将该参数的小写版本添加一个新的参数列表中,并保留其他参数不变。在钩子方法的结束部分,我使用新参数列表对修饰对象调用原始方法。此修饰符会将所有字符串参数转换为小写形式,而与方法或它的参数无关。清单 8 显示了一个应用示例,它对 清单 1 中的日志程序进行了封装:
清单 8. 使用 GenericLowerDecorator 操作 Logger
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 代码设置后期处理。虽然说并不是不可能,但其流程可能会是难以想象的麻烦。
适配器模式
四人组的书将适配器模式定义为:
将某个类的接口转换为接口客户所需的类型。适配器允许各类共同工作(由于接口不兼容,因为无法通过其他方式实现此目的)。
如果您使用过 Swing 中的事件处理程序,则应该对适配器模式有一定的了解。它用于围绕包含多个方法的事件处理接口创建适配器类,这样您就不需要创建自己的类,实现接口,以及导入大量空方法。Swing 适配器允许您子类化适配器,并且可以仅重写处理事件所需的方法。
Groovy 中的适配
从根本上说,适配器模式尝试解答的问题是:“我能否让这个方形木条适合这个圆孔?”这正是本文将要解决的问题。我将使用两个不同的实现,分别强调了表达性对于各语言的重要性。第一个实现将使用 Groovy;清单 9 给出了相关的三个类和一个接口:
清单 9. 方形木条和圆孔
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 类,它封装了方形木条并实现了 RoundHole 的 pegFits() 方法所所需的 getRadius() 方法。但是,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 都支持开放开放类,因此您可以直接在相关类中添加所需的方法。Ruby 中的方形木条和圆孔实现(通过 JRuby)如清单 11 所示:
清单 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 类已经包含一个与圆孔没有任何关系的 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)是一种解释语言,它允许您在解释过程中 执行代码。某些 Ruby 爱好者将 Ruby 中的结构(constructs)称为 “一等市民”,表示该语言的所有部分都是随时可用的。此处的魔力在于(类似于关键字)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 方法中创建的代码块的作用域重新定义了原始(重命名的)方法。最后的 ensure 代码块是 Ruby 版本的 finally 代码块。
此练习的目的并不是深入探讨 Ruby 中的充满魔力的元编程,而是演示表达性极佳的语言能实现哪些功能。解释语言始终比编译语言具有更大的优势,因为它们可以执行编译语言无法执行的代码。事实上,Groovy 引入了一种编译时元编程机制,即 AST Transformations。借助该机制,您可以编写与编译器交互的代码(见 参考资料)。
结束语
本文的所有这些论述证明了什么呢?在各种语言中,表达性就相当于其能力。本文中介绍的许多技巧都是 Java 语言所不支持的,甚至使用 Javassist 等工具提供的字节码生成功能也无法从技术上实现它们(参见 参考资料)。但是,使用这些机制来解决问题可以说是极其麻烦的。此态度也影响了惯用模式。即便您可以看到特定于应用程序的模式,但如果获得收益的方法过于困难,则会让您的项目背上数不清的技术债务。表达性对计算机语言的重要性是不言而喻的!
参考资料 学习
获得产品和技术
讨论
关于作者  | 
|  | Neal Ford 是一家全球性 IT 咨询公司 ThoughtWorks 的软件架构师和 Meme Wrangler。他的工作还包括设计和开发应用程序、教材、杂志文章、课件和视频/DVD 演示,而且他是各种技术书籍的作者或编辑,包括最近的新书
The Productive Programmer
。他主要的工作重心是设计和构建大型企业应用程序。他还是全球开发人员会议上的国际知名演说家。请访问他的 Web 站点。 |
对本文的评价
|