本功能性思维专栏文章将继续我对“四人一组”(GoF)设计模式的替代功能性解决方案的研究(请参阅参考资料 )。 在本文中,我研究了其中最少了解但功能最强大的模式: Interpreter 。
口译员的定义是:
给定一种语言,请定义其语法的表示形式以及使用该表示形式来解释该语言句子的解释器。
换句话说,如果您使用的语言不适合该问题,请使用它来构建适合的语言。 这种方法的好例子出现在Web框架中,例如Grails和Ruby on Rails(请参阅参考资料 ),它们扩展了它们的基本语言(分别是Groovy和Ruby),使编写Web应用程序变得更加容易。
很少理解这种模式,因为构建新语言并不常见,因此所需的技能和习语是专门的。 它是设计模式中最强大的,因为它鼓励您将编程语言扩展到您要解决的问题。 这是Lisp(因此也是Clojure)世界中的一种普遍的精神,但在主流语言中却很少见。
当使用不允许对语言本身进行扩展的语言(例如Java)时,开发人员倾向于将自己的想法塑造成该语言的语法; 这是您唯一的选择。 但是,当您习惯于使用可以轻松扩展的语言时,您会开始将语言转向问题解决方案,而不是反过来。
Java缺乏直接的语言扩展机制,除非您诉诸于面向方面的编程。 但是,下一代JVM语言(Groovy,Scala和Clojure)(请参阅参考资料 )都允许以各种方式进行扩展。 这样,它们满足了解释器设计模式的意图。 首先,我展示了如何以所有三种语言实现操作符重载,然后展示了Groovy和Scala如何使您扩展现有的类。
运算符重载
函数式语言的一个共同特征是运算符重载 -重新定义运算符(例如+
, -
或*
)以使用新类型并表现出新行为的能力。 忽略运算符重载是Java形成时期的一个有意识的决定,但是实际上,每种现代语言现在都具有它,包括JVM上Java的自然继承者。
Groovy
Groovy尝试将Java的语法更新到当前世纪,同时保留其自然语义。 因此,Groovy通过自动将运算符映射到方法名称来允许运算符重载。 例如,如果要重载Integer
的+
运算符,则重写Integer
类的plus()
方法。 完整的映射列表可在线获得(请参阅参考资料 )。 表1显示了部分列表:
表1. Groovy运算符/方法映射的部分列表
操作员 | 方法 |
---|---|
x + y | x.plus(y) |
x * y | x.multiply(y) |
x / y | x.div(y) |
x ** y | x.power(y) |
作为运算符重载的示例,我将在Groovy和Scala中创建一个ComplexNumber
类。 复数是一个具有实部和虚部的数学概念,通常写为3 + 4i
。 复数在许多科学领域都很常见,包括工程,物理,电磁学和混沌理论。 在那些领域中编写应用程序的开发人员将受益于创建能够映射其问题域的运算符的能力。 (有关复数的更多信息,请参见参考资料 。)
清单1中显示了一个Groovy ComplexNumber
类:
清单1. Groovy中的ComplexNumber
package complexnums
class ComplexNumber {
def real, imaginary
public ComplexNumber(real, imaginary) {
this.real = real
this.imaginary = imaginary
}
def plus(rhs) {
new ComplexNumber(this.real + rhs.real, this.imaginary + rhs.imaginary)
}
def multiply(rhs) {
new ComplexNumber(
real * rhs.real - imaginary * rhs.imaginary,
real * rhs.imaginary + imaginary * rhs.real)
}
String toString() {
real.toString() + ((imaginary < 0 ? "" : "+") + imaginary + "i").toString()
}
}
在清单1中 ,我创建了一个同时包含实部和虚部的类,并创建了重载的plus()
和multiply()
运算符。 将两个复数相加很简单: plus()
运算符将两个数字各自的实部和虚部相加以产生结果。 将两个复数相乘需要以下公式:
(x + yi)(u + vi) = (xu - yv) + (xv + yu)i
清单1中的multiply()
运算符复制了该公式。 它将数字的实部相乘,然后减去虚部的乘积,该乘积将相乘于实部和虚部的乘积。
清单2测试了复数运算符:
清单2.测试复数运算符
package complexnums
import org.junit.Test
import static org.junit.Assert.assertTrue
import org.junit.Before
class ComplexNumberTest {
def x, y
@Before void setup() {
x = new ComplexNumber(3, 2)
y = new ComplexNumber(1, 4)
}
@Test void plus_test() {
def z = x + y;
assertTrue 3 + 1 == z.real
assertTrue 2 + 4 == z.imaginary
}
@Test void multiply_test() {
def z = x * y
assertTrue(-5 == z.real)
assertTrue 14 == z.imaginary
}
}
在清单2中, plus_test()
和multiply_test()
方法使用重载操作符的-这两者都是由领域专家使用相同的符号表示-是从内置类型类似用途的不可区分的。
Scala(和Clojure)
Scala通过放弃运算符和方法之间的区别来允许运算符重载:运算符只是具有特殊名称的方法。 因此,要覆盖Scala中的乘法运算符,请覆盖*
方法。 我在清单3的Scala中创建复数:
清单3. Scala中的复数
class ComplexNumber(val real:Int, val imaginary:Int) {
def +(operand:ComplexNumber):ComplexNumber = {
new ComplexNumber(real + operand.real, imaginary + operand.imaginary)
}
def *(operand:ComplexNumber):ComplexNumber = {
new ComplexNumber(real * operand.real - imaginary * operand.imaginary,
real * operand.imaginary + imaginary * operand.real)
}
override def toString() = {
real + (if (imaginary < 0) "" else "+") + imaginary + "i"
}
}
清单3中的类包括熟悉的real
和imaginary
成员,以及+
和*
运算符/方法。 如清单4所示,我自然可以使用ComplexNumber
:
清单4.在Scala中使用复数
val c1 = new ComplexNumber(3, 2)
val c2 = new ComplexNumber(1, 4)
val c3 = c1 + c2
assert(c3.real == 4)
assert(c3.imaginary == 6)
val res = c1 + c2 * c3
printf("(%s) + (%s) * (%s) = %s\n", c1, c2, c3, res)
assert(res.real == -17)
assert(res.imaginary == 24)
通过统一运算符和方法,Scala使运算符重载变得微不足道。 Clojure使用相同的机制来重载运算符。 例如,以下Clojure代码定义了一个重载**
运算符:
(defn ** [x y] (Math/pow x y))
扩展课程
与运算符重载类似,下一代JVM语言允许您以Java语言本身无法实现的方式扩展类(包括核心Java类)。 这些功能通常用于构建特定于域的语言(DSL)。 尽管GoF从未考虑过DSL(因为在当时流行的语言中并不常见),但DSL却例证了Interpreter设计模式的初衷。
通过将单元和其他修饰符添加到诸如Integer
类的核心类中,您可以(与添加运算符一样)更紧密地模拟现实世界中的问题。 Groovy和Scala都允许这样做,但是机制不同。
Groovy的Expando
和类别类
Groovy包括两种向现有类添加方法的机制: ExpandoMetaClass
和Categories 。 (在上一期中 ,我在Adapter模式的上下文中介绍了ExpandoMetaClass
详细信息。)
假设您的公司出于奇怪的遗留原因,需要以每两周Furlongs的速度来表示速度,而不是以每小时英里数(MPH)来表达,并且开发人员发现自己经常进行这种转换。 使用Groovy的ExpandoMetaClass
,您可以向Integer
添加一个FF
属性来处理转换,如清单5所示:
清单5.使用ExpandoMetaClass
向Integer
添加Furlongs / fortnight单元
static {
Integer.metaClass.getFF { ->
delegate * 2688
}
}
@Test void test_conversion_with_expando() {
assertTrue 1.FF == 2688
}
ExpandoMetaClass
的替代方法是创建一个类别包装器类,该概念是从Objective-C借用的。 在清单6中,我向Integer
添加了一个(小写的) ff
属性:
清单6.通过类别类添加单位
class FFCategory {
static Integer getFf(Integer self) {
self * 2688
}
}
@Test void test_conversion_with_category() {
use(FFCategory) {
assertTrue 1.ff == 2688
}
}
类别类是具有静态方法集合的常规类。 每种方法都接受至少一个参数。 第一个参数是此方法增加的类型。 例如,在清单6中 , FFCategory
类具有一个getFf()
方法,该方法接受一个Integer
参数。 当将此类别类与use
关键字一起use
,代码块内的所有适当类型都会增加。 在单元测试中,我可以在代码块内引用ff
属性(记住,Groovy会自动将get
方法转换为属性引用),如清单6底部所示 。
有两种机制可供选择,您可以更精确地控制增强范围。 例如,如果整个系统使用MPH作为默认速度单位,但还需要每两周频繁转换为弗隆,那么使用ExpandoMetaClass
进行全局更改将是适当的。
您可能对重新打开核心JVM类的有用性持怀疑态度,而担心它的广泛影响。 类别类使您可以限制潜在危险的增强功能的范围。 这是一个来自现实世界的开源项目的示例,该示例很好地利用了这种机制。
该项目的easyb(见相关主题 ),您可以编写验证测试的类方面的测试。 考虑清单7中显示的easyb测试的片段:
清单7. easyb测试queue
类
it "should dequeue items in same order enqueued", {
[1..5].each {val ->
queue.enqueue(val)
}
[1..5].each {val ->
queue.dequeue().shouldBe(val)
}
}
queue
类不包含shouldBe()
在测试的验证阶段调用的shouldBe()
方法。 easyb框架为我添加了方法。 清单8中显示的easyb源代码中的it()
方法定义显示了如何:
清单8. easyb的it()
方法定义
def it(spec, closure) {
stepStack.startStep(BehaviorStepType.IT, spec)
closure.delegate = new EnsuringDelegate()
try {
if (beforeIt != null) {
beforeIt()
}
listener.gotResult(new Result(Result.SUCCEEDED))
use(categories) {
closure()
}
if (afterIt != null) {
afterIt()
}
} catch (Throwable ex) {
listener.gotResult(new Result(ex))
} finally {
stepStack.stopStep()
}
}
class BehaviorCategory {
// ...
static void shouldBe(Object self, value) {
shouldBe(self, value, null)
}
//...
}
在清单8中 , it()
方法接受一个spec(描述测试的字符串)和一个代表测试主体的闭包块。 在方法的中点,闭包在BehaviorCategory
块内执行,该块出现在列表的底部。 BehaviorCategory
扩展了Object
,从而允许Java Universe中的任何实例验证其值。
通过允许驻留在层次结构顶部的Object
选择性扩充,Groovy的开放类机制使得可以轻松验证任何实例的结果,但限制了use
块主体的变化。
Scala的隐式强制转换
Scala使用隐式强制转换来模拟现有类的扩充。 隐式强制转换不会在类中添加方法,但是允许语言将对象自动转换为具有所需方法的适当类型。 例如,我无法将添加isBlank()
方法将String
类,但我可以创建自动转换的隐式转换String
s到一类确实有该方法。
作为示例,我想向Array
添加append()
方法,这使我可以轻松地将Person
实例添加到适当类型的数组中,如清单9所示:
清单9.向Array
添加方法以添加人员
case class Person (firstName: String, lastName: String) {}
class PersonWrapper(a: Array[Person]) {
def append(other: Person) = {
a ++ Array(other)
}
def +(other: Person) = {
a ++ Array(other)
}
}
implicit def listWrapper(a: Array[Person]) = new PersonWrapper(a)
在清单9中 ,我创建了一个带有几个属性的简单Person
类。 为了使Array[Person]
(在Scala中,泛型使用[ ]
而不是< >
作为定界符)认识Person
,我创建了PersonWrapper
类,其中包括所需的append()
方法。 在清单的底部,我创建了隐式转换,当我在数组上调用append()
方法时,该转换将自动将Array[Person]
转换为PersonWrapper
。 清单10测试了转换:
清单10.测试现有类的自然扩展
val p1 = new Person("John", "Doe")
var people = Array[Person]()
people = people.append(p1)
在清单9中 ,我还向PersonWrapper
类添加了+
方法。 清单11显示了如何使用这个非常直观的运算符版本:
清单11.修改语言以增强可读性
people = people + new Person("Fred", "Smith")
for (p <- people)
printf("%s, %s\n", p.lastName, p.firstName)
Scala实际上并没有向原始类中添加方法,但是它通过自动转换为合适的类型来提供这样做的外观。 Scala需要使用类似Groovy之类的语言进行元编程所需的相同工作,以避免使用过多的隐式强制转换创建互连类的复杂网络。 但是,如果使用正确,则隐式强制转换可以帮助您编写非常有表现力的代码。
结论
GoF最初的Interpreter设计模式建议创建一种新语言,但是它们的基本语言不支持我们今天可以使用的优雅扩展机制。 所有下一代Java语言都使用多种技术来支持语言级别的可扩展性。 在本期中,我演示了Groovy,Scala和Clojure中运算符重载的工作方式,并研究了Groovy和Scala中的类扩展。
在以后的文章中,我将展示Scala样式模式匹配和泛型的组合如何使您能够替换几个传统的设计模式。 在该讨论中必不可少的是一个概念,该概念在功能风格的错误处理中也将发挥作用,这是下一部分的主题。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft12/index.html