函数式思维: 函数设计模式,第 3 部分

解释器模式和扩展语言

Gang of Four 的解释器设计模式 (Interpreter design pattern) 鼓励在一个语言的基础上构建一个新的语言来实现扩展。大多数函数式语言都能够让您以多种方式(如操作符重载和模式匹配)对语言进行扩展。尽管 Java™ 不支持这些技术,下一代 JVM 语言均支持这些技术,但其具体实现细则有所不同。在本文中,Neal Ford 将探讨 Groovy、Scala 和 Clojure 如何通过以 Java 无法支持的方式来实现函数式扩展,从而实现解释器设计模式的目的。

Neal Ford, 软件架构师, ThoughtWorks Inc.

2012 年 6 月 11 日

  • +内容

关于本系列

本系列的目标是重新调整您对函数式思维的认识,帮助您以全新的方式看待常见问题,并提升您的日常编码能力。本系列文章将探讨函数式编程概念、允许在 Java 语言中进行函数式编程的框架、在 JVM 上运行的函数编程语言,以及语言设计的未来方向。本系列面向那些了解 Java 及其抽象工作原理,但对函数语言不甚了解的开发人员。

在本期 函数式思维 的文章中,我将继续研究 Gang of Four (GoF) 设计模式(参阅 参考资料)的函数式替代解决方案。在本文中,我将研究最少人了解,但却是最强大的模式之一:解释器 (Interpreter)

解释器的定义是:

给定一个语言,定义其语法表示,以及一个使用该表示来解释语言中的句子的解释器。

换句话说,如果您正在使用的语言不适用于解决问题,那么用它来构建一个适用的语言。关于该方法的一个很好的示例出现在 Web 框架中,如 Grails 和 Ruby on Rails(参阅 参考资料),它们扩展了自己的基础语言(分别是 Groovy 和 Ruby),使编写 Web 应用程序变得更容易。

这种模式最少人了解,因为构建一种新的语言并不常见,需要专业的技能和惯用语法。它是最强大的 设计模式,因为它鼓励您针对正在解决的问题扩展自己的编程语言。这在 Lisp(因此 Clojure 也同样)世界是一个普遍的特质,但在主流语言中不太常见。

当使用禁止对语言本身进行扩展的语言(如 Java)时,开发人员往往将自己的思维塑造成该语言的语法;这是您的惟一选择。然而,当您渐渐习惯使用允许轻松扩展的语言时,您就会开始将语言折向解决问题的方向,而不是其他折衷的方式。

Java 缺乏直观的语言扩展机制,除非您求助于面向方面的编程。然而,下一代的 JVM 语言(Groovy、Scala 和 Clojure)(参阅 参考资料)均支持以多种方式进行扩展。通过这样做,它们可以达到解释器设计模式的目的。首先,我将展示如何使用这三种语言实现操作符重载,然后演示 Groovy 和 Scala 如何让您扩展现有的类。

操作符重载(operator overloading)

操作符重载 是函数式语言的一个常见特性,能够重定义操作符(如 +- 或 *)配合新的类型工作,并表现出新的行为。操作符重载的缺失是 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 却体现了解释器设计模式的初衷。

通过将计量单位和其他修饰符添加给 Integer 等核心类,您可以(就像添加操作符一样)更紧密地对现实问题进行建模。Groovy 和 Scala 都支持这样做,但它们使用不同的机制。

Groovy 的 Expando 和类别类

Groovy 包括两种对现有类添加方法的机制:ExpandoMetaClass 和类别。(在 函数式思维:函数设计模式,第 2 部分 中,我在适配器模式的上下文中详细介绍过 ExpandoMetaClass。)

比方说,您的公司由于离奇的遗留原因,需要以浪(furlongs,英国的计量单位)/每两周而不是以英里/每小时 (MPH) 的方法来表达速度,开发人员发现自己经常要执行这种转换。使用 Groovy 的  ExpandoMetaClass,您可以添加一个 FF 属性给处理转换的 Integer ,如清单 5 所示:

清单 5. 使用 ExpandoMetaClass 添加一个浪/两周的计量单位给 Integer
static {
  Integer.metaClass.getFF { ->
    delegate * 2688
  }
}

@Test void test_conversion_with_expando() {
  assertTrue 1.FF == 2688
}

ExpandoMetaClass 的替代方法是,创建一个类别 包装器类,这是从 Objective-C 借来的概念。在清单 6 中,我添加了一个(小写) ff 属性给Integer

清单 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 关键字一起使用时,代码块内所有相应类型都被增强。在单元测试中,我可以在代码块内引用 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() 方法,这是我在测试的验证阶段所调用的方法。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 世界中的任何 实例验证其值。

通过允许选择性增强驻留在层次结构顶层的 Object,Groovy 的开放类机制可以轻松地实现为任何实例验证结果,但它限制了对 use 块主体的修改。

Scala 的隐式转换

Scala 使用隐式转换 来模拟现有类的增强。隐式转换不会对类添加方法,但允许语言自动将一个对象转换成拥有所需方法的相应类型。例如,我不能将 isBlank() 方法添加到 String 类中,但我可以创建一个隐式转换,将 String 自动转换为拥有这种方法的类。

作为一个示例,我想将 append() 方法添加到 Array,这让我可以轻松地将 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 实际上并未对原始的类添加一个方法,但它通过自动转换成一个合适的类型,提供了这样做的外观。使用 Groovy 等语言进行元编程所需要的相同工作在 Scala 中也需要,以避免过多使用隐式转换而产生由相互关联的类所组成的令人费解的网。但是,在正确使用时,隐式转换可以帮助您编写表达非常清晰的代码。

结束语

来自 GoF 的原始解释器设计模式建议创建一个新语言,但其基础语言并不支持我们今天所掌握的良好扩展机制。下一代 Java 语言都通过使用多种技术来支持语言级别的可扩展性。在本期文章中,我演示了操作符重载如何在 Groovy、Scala 和 Clojure 中工作,并研究了在 Groovy 和 Scala 中的类扩展。

在下期文章中,我将展示 Scala 风格的模式匹配和泛型的组合如何取代一些传统的设计模式。该讨论的中心是一个在函数式错误处理中也起着作用的概念,这一概念将是我们下期文章的主题。

参考资料

学习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 精通Python设计模式需要对Python编程语言有深入的理解,并且熟悉常用的设计模式设计模式是一种解决特定问题的经验总结,可以提供可复用的解决方案。 首先,精通Python设计模式需要掌握Python的基本语法和面向对象编程的概念。了解类、对象、继承、多态等概念并能够熟练运用。 其次,熟悉常用的设计模式设计模式可以分为创建型、结构型和行为型三大类。常见的设计模式包括单例模、工厂模、观察者模、策略模、装饰器模等。对于每个模,需要了解其定义、适用场景和解决的问题,同时能够根据具体的问题选择合适的模。 此外,还需要能够在实际项目中运用设计模式。通过合理地运用设计模式,可以提高代码的可复用性和可扩展性,降低系统的维护成本。在使用设计模式时,需要根据具体的需求和情况进行权衡,避免过度设计和滥用设计模式。 最后,持续学习和实践是精通Python设计模式的关键。设计模式是一种经验,需要在实际开发中不断使用和改进。通过参与开源项目、阅读优秀代码和交流讨论等方,不断提高自己的设计模式能力。 总之,精通Python设计模式需要熟悉Python语言并掌握常用的设计模式,能够在实际项目中灵活运用。通过不断学习和实践,可以进一步提高自己的设计模式水平。 ### 回答2: 精通Python设计模式是指对Python语言中各种设计模式及其应用有深入理解和熟练掌握的能力。 首先,精通Python设计模式需要对Python语言本身有较高的掌握程度,熟悉Python的语法、数据类型、面向对象编程等基础知识。同时,需要了解Python常用的模块和库,如NumPy、Pandas、TensorFlow等,在项目中熟练运用。 其次,要精通Python设计模式,需要对常见的设计模式有深入的理解。设计模式是对软件设计经验的总结和抽象,可以解决某类问题,并且能够提供可复用的设计方案。常见的设计模式有单例模、工厂模、观察者模、装饰器模等。对于每种设计模式,我们需要了解它的定义、适用场景、优缺点以及实际应用中的注意事项等。 最后,精通Python设计模式也需要具备实际项目经验。通过实践中运用设计模式解决具体问题,可以加深对设计模式的理解,并掌握如何将设计模式应用于实际项目中。同时,通过项目经验还可以锻炼解决问题的能力和代码质量。 总而言之,精通Python设计模式是掌握Python语言、了解常见设计模式、具备实际项目经验的综合能力。通过深入学习和实践,可以提高代码的可维护性、复用性和扩展性,从而更好地应对复杂的软件设计和开发任务。 ### 回答3: 精通Python设计模式是指对Python编程语言中各种设计模式的理解和运用达到了高级水平。 首先,理解设计模式的概念是基础。设计模式是在软件开发过程中用于解决常见问题和提供可复用解决方案的经验总结。在精通Python设计模式前,必须熟悉常见的设计模式分类,如创建型模、结构型模和行为型模,并了解每个设计模式的特点和适用场景。 其次,精通Python设计模式需要掌握Python语言的特性和语法。熟练使用Python的面向对象编程(OOP)特性,如封装、继承和多态,能够更好地理解并实现设计模式。同时,熟悉Python中的函数编程(FP)特性,如高阶函数、闭包和装饰器,能够更好地应用某些设计模式,如策略模和装饰器模。 其次,熟悉和掌握各种设计模式的具体实现和应用。例如,对于创建型模中的工厂模,可熟练使用Python的工厂函数或元类实现;对于结构型模中的适配器模,可使用Python的继承或组合方实现;对于行为型模中的观察者模,可利用Python的事件机制或回调函数实现。 最后,精通Python设计模式还需要对代码的质量和可维护性有一定的追求。设计模式并非银弹,不应盲目使用。在实际开发中,需要根据具体情况权衡设计模式的利弊,并结合项目的需求和团队的能力做出合理的选择和折中。 总的来说,精通Python设计模式需要综合考虑设计模式的基本概念、Python语言的特性和语法以及具体应用场景的需求,通过不断实践和学习,不断提高自己的设计思维和编码能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值