紧急设计的主要推动力之一是能够查看和收获惯用模式:在代码库中以非平凡的方式重复的过程,结构和习惯用法。 但是,有时这些模式对您隐藏。 在《 演进式体系结构和紧急设计》系列的第一期中 ,我描述了使这些模式的可见性难以理解的问题,例如猖generic的通用性。 构建多层应用程序对于实现可伸缩性和分区的关注点分离可能是有效的,但是它隐藏了惯用模式,因为现在您必须跨多个层找到它们。 成为一名优秀的设计师和建筑师需要开发“眼睛”以识别这些模式。
语言收获的另一个障碍在于语言本身的表现力。 例如,很难从汇编语言中提取模式,因为该语言的特性会影响表达能力。 即使您已经学会了阅读汇编语言和母语,但编写代码的方式却受到严格的限制,使您无法获得全面的了解。 例如,将变量传入和传出寄存器而不是能够创建命名良好的变量和方法,意味着您要花费大量时间来处理语言固有的开销。
将Java™语言与汇编语言进行比较是一项艰巨的任务,但是计算机语言的表现力却始终存在。 有些语言比其他语言更具表现力,因此更容易有效地查看模式。 为此,本文(两部分的第一部分)使用JVM(Groovy)的动态语言来演示“四人一组”模式的替代实现。
重新审视设计模式
一个在软件开发中的开创性的书是设计模式:由埃里克·伽马,理查德头盔,拉尔夫·约翰逊和约翰·弗利赛德斯可复用面向对象软件的基础 (参见相关主题 )。 本书包括两个不同的部分:对软件开发过程中遇到的常见问题的描述及其解决方案的示例。 第一部分作为一般问题的目录很有价值,但是模式的实现必然显示出对特定语言的偏见。 示例实现在C ++和Smalltalk中都出现,但是它们利用了Smalltalk的一些高级语言功能。 在许多方面,实现强调了C ++的局限性以及解决该语言固有问题所需的解决方法。
“四人帮”一书的术语在今天仍然很有价值,但是实现已过时。 这些实现在结构上解决的许多问题(通过构建交互类的层次结构)在提供更多功能和表达能力的语言中具有更优雅的解决方案。
自《四人帮》出版以来,又发生了另一个有趣的变化。 许多语言已经将模式包含在语言本身中。 例如,Java语言在JDK 1.1和1.2之间更改了其集合迭代样式,用Iterator
接口替换了Enumerator
,以使Java语言中的迭代器与Gang of Four的Iterator模式更紧密地对齐。 语言趋向于包含模式和其他常见习语,它们在语言抽象本身中就消失了。
前几个示例显示了在更现代的基于Java的语言中对“四人帮”模式的确切接受,将Iterator和Command模式直接合并到该语言中。
迭代器模式
《四人帮》一书将迭代器模式定义为:
提供一种在不暴露其基础表示的情况下顺序访问聚合对象的元素的方法。
Iterator模式是第一个以Iterator
接口和实现形式添加到Java语言中的模式之一。 Groovy进一步迈出了一步,并将内部迭代器添加为collections API的一部分。 因此,可以使用each
方法结合代码块来非常轻松地遍历一个集合,如清单1所示。该清单说明了一个内部迭代器(也称为push迭代器,因为它将每个元素依次推入代码)块)。
清单1. Groovy each
运算符
def numbers = [1,2,3,4]
numbers.each { n ->
println n
}
Groovy允许迭代适用于所有类型的集合,包括哈希,如清单2所示:
清单2.对哈希的迭代
def months = [Mar:31, Apr:30, May:31]
months.each {
println it
}
Groovy还实现了方便的默认行为,即自动为名为it
的迭代提供参数,您可以在代码块中引用该参数。
Groovy支持外部迭代器(也称为拉式迭代器,因为您必须请求集合中的下一个项目),如清单3所示。这与Java语言本身内置的迭代器完全相同。
清单3. Pull迭代器
iterator = numbers.iterator()
while (iterator.hasNext()) {
println iterator.next()
}
迭代器是如此普遍,以至于它根本不再是一种形式化的模式。 这只是语言的功能。 这是计算机语言本身的新兴设计中的常见现象。
命令模式
《四人帮》一书将命令模式定义为:
将请求封装为对象,从而使您可以将具有不同请求,队列或日志请求的客户端参数化,并支持可撤销的操作。
Java语言中此模式的常见实现创建了一个包含execute()
方法的Command
类。 Command设计模式在Groovy中以代码块的形式出现,该代码块是在独立大括号( {
和}
)中定义的任何内容。 您可以通过调用代码块的call()
方法或在包含代码块的变量名称后加上括号(带有或不带有参数)来执行代码块,而不必强迫您创建新的类和相应的方法。 清单4显示了一个示例:
清单4. Groovy中带有代码块的命令模式
def count = 0
def commands = []
1.upto(10) { i ->
commands.add { count++ }
}
println "count is initially ${count}"
commands.each { cmd ->
cmd()
}
println "did all commands, count is ${count}"
支持撤消
与类似的机制(例如匿名内部类)相比,使用代码块的优势之一在于其简洁。 因为指定不可撤消的操作是这样的普遍需求,所以语法变得很重要。 考虑清单5中的Groovy代码,该代码显示了如何使用代码块结合Command设计模式来支持可撤消操作:
清单5.使用代码块来支持不可撤销的操作
class Command {
def cmd, uncmd
Command(doCommand, undoCommand) {
cmd = doCommand
uncmd = undoCommand
}
def doCommand() {
cmd()
}
def undoCommand() {
uncmd()
}
}
def count = 0
def commands = []
1.upto(10) { i ->
commands.add(new Command({count++}, {count--}))
}
println "count is initially ${count}"
commands.each { c -> c.doCommand() }
commands.reverseEach { c -> c.undoCommand() }
println "undid all commands, count is ${count}"
commands.each { c -> c.doCommand() }
println "redid all command, count is ${count}"
传递代码块作为参数很简单,允许简洁但仍可读的commands.add(new Command({count++}, {count--}))
。add commands.add(new Command({count++}, {count--}))
语法。
代码块,表现力和惯用模式
尽管代码块和匿名内部类之间的区别似乎只是语义,但它会影响代码的可读性,并因此而影响惯用模式的难易程度。 考虑这个惯用模式的例子,我称之为工作单元 。 首先,清单6中显示了Java版本(使用匿名内部类):
清单6.具有匿名内部类的工作单元模式
public void wrapInTransaction(Command c) throws SQLException {
setupDataInfrastructure();
try {
c.execute();
completeTransaction();
} catch (RuntimeException ex) {
rollbackTransaction();
throw ex;
} finally {
cleanUp();
}
}
public void addOrderFrom(final ShoppingCart cart, final String userName,
final Order order) throws SQLException {
wrapInTransaction(new Command() {
public void execute() throws SQLException{
add(order, userKeyBasedOn(userName));
addLineItemsFrom(cart, order.getOrderKey());
}
});
}
现在,考虑清单7所示的用Groovy编写的相同示例,该示例利用了代码块提供的更简洁的语法:
清单7.使用代码块实现工作单元模式
public class OrderDbClosure {
def wrapInTransaction(command) {
setupDataInfrastructure()
try {
command()
completeTransaction()
} catch (RuntimeException ex) {
rollbackTransaction()
throw ex
} finally {
cleanUp()
}
}
def addOrderFrom(cart, userName, order) {
wrapInTransaction {
add order, userKeyBasedOn(userName)
addLineItemsFrom cart, order.getOrderKey()
}
}
}
尽管在代码清单7定义wrapInTransaction()
是大致相同的清单6 ,调用代码是更清洁。 Java版本需要创建许多语法来实现匿名内部类。 该语法模糊了我要完成的意思。 您必须花更多的语法来查看设计元素,才更难意识到存在模式。 Groovy版本使用最少的语法来实现该模式,仅保留相关内容。
策略模式
《四人帮》一书将策略模式定义为:
定义一系列算法,封装每个算法,并使它们可互换。 策略使算法独立于使用该算法的客户端而变化。
用Java语言实现的Strategy模式的传统实现需要一个接口,该接口定义算法的语义,并提供提供实现的具体类。 清单8中显示了Strategy的Java实现。
清单8. Java语言中的乘法策略
public interface Calc {
public int product(int x, int y);
}
public class CalcByMult implements Calc {
public int product(int x, int y) {
return x * y;
}
}
public class CalcByAdds implements Calc {
public int product(int x, int y) {
int result = 0;
for (int i = 1; i <= y; i++)
result += x;
return result;
}
}
Java语言迫使您创建结构来解决问题。 实际上,“四人帮”解决方案在创建结构以实施模式解决方案方面有很大的偏见-您是否注意到每个模式都包含一个显示解决方案的UML图? 但是建筑结构并不总是解决问题的最清晰或简洁的方法。 考虑清单9,它在Groovy中实现了相同的模式:
清单9. Groovy中的乘法策略
interface Calc {
def product(n, m)
}
def multiplicationStrategies = [
{ n, m -> n * m } as Calc,
{ n, m -> def result = 0
n.times { result += m }
result
} as Calc
]
def sampleData = [
[3, 4, 12],
[5, -5, -25]
]
sampleData.each{ data ->
multiplicationStrategies.each{ calc ->
assert data[2] == calc.product(data[0], data[1])
}
}
在Groovy示例中,不需要显式创建额外的类来实现定义调用语义的接口。 Groovy中功能强大的as
运算符采用一个代码块,并生成一个实现该接口的新类,然后您就可以调用该类,就好像它是实现该接口的具体类一样。 因此,在此示例执行中,所有动态定义策略的代码块仍可以像实现Calc
接口的形式具体类一样工作。
口译模式
四个口译帮派模式是一个特例。 定义是:
给定一种语言,请定义其语法的表示形式以及使用该表示形式来解释该语言句子的解释器。
此模式本质上是“摆脱监狱”模式。 它是由东西菲利普·格林斯珀更好的拥护作为格林斯潘第十定律(见正式加入相关主题 ):
任何足够复杂的C或Fortran程序都包含一个临时的,非正式指定的,bug缠身的,缓慢实现的Common Lisp一半。
他的意思是,当您用较弱的语言构建越来越多的复杂软件时,实际上是在从更强大的语言(如Lisp)中实现即席功能,一次只能实现一项功能,而没有意识到。 解释器模式是承认您的基本语言可能不足以满足当前的任务,在这种情况下,最好的解决方案是使用该语言在其之上构建更好的语言。
这种模式显示了“四人帮”一书的年代及其思想。 帮派提倡放弃您的核心语言,并在其上构建一种全新的语言,创建自己的词法分析器,解析器,语法等。 但是,在过去的几年中,这种模式的中间阶段已经成为主流(即使自Lisp以来就已经存在):通过在其之上构建特定领域的语言(DSL),使您的语言更具表现力。
在Java语言之上构建DSL是困难的,因为语言语法非常严格,并且几乎没有语言级别的扩展点。 用Groovy和Ruby之类的语言构建DSL是很常见的,因为该语法既可扩展,又更宽容。
清单10显示了一个用Groovy编写的小配方DSL的演示应用程序:
清单10. Groovy中的食谱DSL
def recipe = new Recipe("Spicy Bread")
recipe.add 1.gram.of("Nutmeg")
recipe.add 2.lbs.of("Flour")
println recipe
清单10中有趣的代码行是中间的几行,它们定义了配方的成分。 Groovy允许您向任何类添加新方法(包括java.lang.Integer
,这是Groovy处理数字文字的方式)。 这就是我能够在数值上调用方法的方式。 要将新方法添加到现有类中,可以使用称为ExpandoMetaClass
的Groovy机制,如清单11所示:
清单11.通过ExpandoMetaClass
向Integer
添加方法
Integer.metaClass.getGram { ->
delegate
}
Integer.metaClass.getGrams {-> delegate.gram }
Integer.metaClass.getPound { ->
delegate * 453.29
}
Integer.metaClass.getPounds {-> delegate.pound }
Integer.metaClass.getLb {-> delegate.pound }
Integer.metaClass.getLbs {-> delegate.pound }
在清单11中 ,我在Integer
的元类上定义了一个名为getGram
的新属性(该属性使我可以从Groovy调用它而不使用get
前缀)。 在属性定义中, delegate
引用此Integer
实例的值; 我将所有度量单位都保留在DSL内,以克为单位,因此它返回整数值。 DSL的目标之一是流畅性,因此我还定义了称为getGrams
的gram
属性的复数形式,从而使DSL代码更具可读性。 我还需要支持英镑作为度量单位,因此我还定义了一系列pound
属性。
新属性处理了DSL的第一部分,而of
方法则是剩下的唯一部分。 of
也是添加到Integer
的方法,它显示在清单12中。该方法接受一个参数,分配配料的名称,设置数量,并返回新创建的配料对象。
清单12.添加到Integer
的of
方法
Integer.metaClass.of { name ->
def ingredient = new Ingredient(name);
ingredient.quantity = delegate
ingredient
}
一些微妙之处存在于代码清单10由该代码在暴露清单12 。 尽管DSL的第一行现在可以正常工作( recipe.add 1.gram.of("Nutmeg")
),但是第二行失败了,因为我定义的of
方法不再适用。 一旦在样recipe.add 2.lbs.of("Flour")
of
发生了对的调用,调用类型便从Integer
更改为BigDecimal
,这是Groovy浮点数的默认格式。 这怎么发生的? 在pounds
调用中,返回类型现在是浮点数(2 * 453.29)。 因此,我需要附加到BigDecimal
of
方法,如清单13所示:
清单13.添加到BigDecimal
的of
方法
BigDecimal.metaClass.of { name ->
def ingredient = new Ingredient(name);
ingredient.quantity = delegate
ingredient
}
在DSL实现中,此问题经常突然出现。 许多DSL需要很多东西:1周,2磅,6美元。 向Integer
添加方法使您可以创建更具表现力的代码,因为您可以使用实数表示数值。 DSL中的代码行通常以数量开头,调用一些中间方法来完成工作,最后返回有趣的最终类型的实例。 在清单10中 ,数量启动方法调用,该方法调用通过Integer
,然后通过BigDecimal
,最后返回Ingredient
。 DSL倾向于创建更紧凑的代码,从而消除了不必要的冗长语法。 删除语法有助于提高可读性,从而使隐藏在设计中的设计元素更容易被所需但混乱的语法所遮盖。
第1部分的结论
在本期中,我介绍了语言的表达方式如何影响可读性以及在代码中查看(并因此收获)惯用模式的能力-您要查找和重用的真实设计元素。 我讨论了如何用更具表现力的语言(例如Groovy)实现几种“四人帮”模式。 在第2部分中,我将继续讨论表达能力和语言的交集,可以更好地表达形式设计模式的方式,以及某些语言如何为您提供在表达能力较弱的语言中实际上不存在的功能。
翻译自: https://www.ibm.com/developerworks/java/library/j-eaed7/index.html