哪些GOF设计模式在Java和Scala中有完全不同的实现?

32

最近我阅读了以下的 SO 问题:

是否存在在 Scala 中使用访问者模式的用例?如果我在 Java 中会使用访问者模式,那么我在 Scala 中应该每次都使用模式匹配吗?

这是问题的链接:Scala 中的访问者模式,被接受的答案开始说:

是的,你可能应该首先使用模式匹配而不是访问者模式。参见这里http://www.artima.com/scalazine/articles/pattern_matching.html

我的问题(受上述问题启发)是哪些 GOF 设计模式在 Scala 中有完全不同的实现?如果我在 Scala 中编程,应该注意什么,不要遵循基于 Java 的设计模式(Gang of Four)的编程模型?

创建型模式

  • 抽象工厂
  • 构建器
  • 工厂方法
  • 原型
  • 单例:直接创建对象 (Scala)

结构型模式

  • 适配器
  • 桥接
  • 组合
  • 装饰器
  • 外观
  • 享元
  • 代理

行为型模式

  • 职责链
  • 命令
  • 解释器
  • 迭代器
  • 中介者
  • 备忘录
  • 观察者
  • 状态
  • 策略
  • 模板方法
  • 访问者:模式匹配 (Scala)

1
很多这些设计模式只是为了弥补缺乏一等函数而存在的。 - Daniel C. Sobral
@Daniel C. Sobral先生,我应该重点研究哪些模式,并将其替换为一流的函数?目前,我的思维结构是以这些设计模式来解决编程任务。 - Optimight
@DanielC.Sobral,我认为头等函数使实现这些模式更容易。在大多数情况下,我不认为拥有头等函数会完全使模式无效。 - The Archetypal Paul
1
请查看Neal Ford的函数式设计模式文章:https://www.ibm.com/developerworks/java/library/j-ft10/index.html https://www.ibm.com/developerworks/java/library/j-ft11/index.html - Brian
你可以查看http://pavelfatin.com/design-patterns-in-scala/。 - Pavel Fatin
有关Scala中适配器模式的更多信息 - http://www.maxondev.com/adapter-design-pattern-scala-implicits/ - Maxim
3个回答

46
几乎所有这些模式都有Scala替代方案,涵盖了一些但不是全部用例。当然,这是我个人的看法:

创建型模式

建造者模式

Scala可以通过泛型类型更优雅地实现此功能,而Java则无法做到,但总体思路相同。在Scala中,该模式最简单的实现方式如下:

trait Status
trait Done extends Status
trait Need extends Status

case class Built(a: Int, b: String) {}
class Builder[A <: Status, B <: Status] private () {
  private var built = Built(0,"")
  def setA(a0: Int) = { built = built.copy(a = a0); this.asInstanceOf[Builder[Done,B]] }
  def setB(b0: String) = { built = built.copy(b = b0); this.asInstanceOf[Builder[A,Done]] }
  def result(implicit ev: Builder[A,B] <:< Builder[Done,Done]) = built
}
object Builder {
  def apply() = new Builder[Need, Need]
}

如果您在REPL中尝试此操作,请确保Builder类和对象在同一块中定义,即使用:paste。通过使用<:<检查类型、泛型类型参数和case类的复制方法,可以形成非常强大的组合。

工厂方法(和抽象工厂方法)

工厂方法的主要用途是使您的类型保持清晰;否则,您可能会使用构造函数。由于Scala强大的类型系统,您不需要帮助来保持类型清晰,因此您可以使用构造函数或类伴生对象中的apply方法来创建事物。特别是在伴生对象的情况下,保持接口一致的难度与保持工厂对象中的接口一致的难度相同。因此,大多数工厂对象的动机已经消失。

同样,许多抽象工厂方法的情况可以通过使类伴生对象继承一个适当的trait来替换。

原型

当然,重写方法等在Scala中也有它们的用处。但是Design Patterns网站上用于原型模式的示例在Scala(或Java IMO)中不太可取。但是,如果你希望超类根据其子类选择操作而不是让它们自己决定,那么你应该使用match而不是笨拙的instanceof测试。
单例
Scala使用object来实现单例——使用和享受吧!
结构型模式
适配器
Scala的trait在这里提供了更强大的功能——例如,你可以创建一个只实现接口的部分的特质,留下其他部分由你来定义。例如,java.awt.event.MouseMotionListener要求你填写两个方法:
def mouseDragged(me: java.awt.event.MouseEvent)
def mouseMoved(me: java.awt.event.MouseEvent)

也许您想忽略拖动。那么您可以编写一个特质trait:
trait MouseMoveListener extends java.awt.event.MouseMotionListener {
  def mouseDragged(me: java.awt.event.MouseEvent) {}
}

现在您可以从中继承并仅实现mouseMoved。因此:使用类似的模式,但使用Scala更加强大。
桥接器
您可以在Scala中编写桥接器。虽然不像Java那样糟糕,但这需要大量的样板文件。我不建议经常使用它作为抽象的一种方法;首先请仔细考虑您的接口。请记住,随着特质的增强功能,您通常可以在否则可能会编写桥接器的地方使用它们来简化更复杂的接口。
在某些情况下,您可能希望编写接口转换器而不是Java桥接器模式。例如,也许您想要使用相同的接口处理鼠标拖动和移动,只有一个布尔标志区分它们。然后您可以
trait MouseMotioner extends java.awt.event.MouseMotionListener {
  def mouseMotion(me: java.awt.event.MouseEvent, drag: Boolean): Unit
  def mouseMoved(me: java.awt.event.MouseEvent) { mouseMotion(me, false) }
  def mouseDragged(me: java.awt.event.MouseEvent) { mouseMotion(me, true) }
}

这让您跳过大部分桥接模式样板代码,同时实现高度独立性,并仍然让您的类遵守原始接口(因此您不必保持包装和解包装)。

组合模式

使用 case 类可以轻松实现组合模式,但更新可能相当费力。在 Scala 和 Java 中同样有价值。

装饰器模式

装饰器模式很棘手。通常情况下,您不想在继承不是您想要的情况下在不同类上使用相同的方法;您真正想要的是在同一类上有一个不同的方法,该方法执行您想要的而不是默认的操作。enrich-my-library pattern 往往是一个更好的替代品。

外观模式

外观模式在 Scala 中比 Java 更好,因为您可以使特质携带部分实现,因此在组合它们时不必全部自己完成。

享元模式

尽管在Scala中与Java一样,使用轻量级的概念,但是你可以使用更多工具来实现它:lazy val,只有在需要时才创建变量(并且此后重复使用),以及by-name parameters,仅在函数实际使用该值时才执行创建函数参数所需的工作。尽管如此,在某些情况下,Java模式保持不变。

代理模式

在Scala中与Java相同。

行为模式

责任链模式

在那些可以按顺序列出负责方的情况下,你可以

xs.find(_.handleMessage(m))

假设每个人都有一个handleMessage方法,如果消息已处理,则返回true。如果您想在消息传递过程中对其进行更改,请使用fold。

由于很容易将责任方放入某种缓冲区中,因此Java解决方案中使用的复杂框架在Scala中很少有用处。

命令

这种模式几乎完全被函数取代。例如,可以使用以下方式代替所有内容:

public interface ChangeListener extends EventListener {
  void stateChanged(ChangeEvent e)
}
...
void addChangeListener(ChangeListener listener) { ... }

你只需要

def onChange(f: ChangeEvent => Unit)

解释器

Scala提供了解析器组合器,比简单的解释器设计模式要强大得多。

迭代器

Scala在其标准库中内置了Iterator。几乎可以轻松地使自己的类扩展IteratorIterable;后者通常更好,因为它使重用变得简单。这绝对是一个好主意,但如此直截了当,我几乎不会称之为一种设计模式。

中介者

这在Scala中可以很好地工作,但通常适用于可变数据,即使中介者也可能因使用不当而遇到竞态条件等问题。相反,尽可能将所有相关数据存储在一个不可变集合、case class或其他类型中,并在需要协调更改的更新时同时更改所有内容。这样做不能帮助您与javax.swing进行接口交互,但在其他情况下广泛适用:

case class Entry(s: String, d: Double, notes: Option[String]) {}

def parse(s0: String, old: Entry) = {
  try { old.copy(s = s0, d = s0.toDouble) }
  catch { case e: Exception => old }
}

当您需要处理多个不同关系(每个关系一个中介者)或具有可变数据时,请使用中介者模式。

备忘录

lazy val 几乎是备忘录模式的许多最简单应用的理想选择,例如:

class OneRandom {
  lazy val value = scala.util.Random.nextInt
}
val r = new OneRandom
r.value  // Evaluated here
r.value  // Same value returned again

你可以创建一个专门用于惰性求值的小类:

class Lazily[A](a: => A) {
  lazy val value = a
}
val r = Lazily(scala.util.Random.nextInt)
// not actually called until/unless we ask for r.value

观察者模式

这是一个相当脆弱的模式。在可能的情况下,最好使用不可变状态(参见中介者模式),或者使用演员模式,其中一个演员向所有其他演员发送有关状态更改的消息,但每个演员都可以处理过时的状态。

状态模式

这在Scala中同样有用,并且实际上是创建没有方法的特征枚举的首选方式:

sealed trait DayOfWeek
final trait Sunday extends DayOfWeek
...
final trait Saturday extends DayOfWeek

通常你希望工作日做些事情来证明这些样板代码的数量是有必要的。

策略

这几乎完全被使用方法来接受实现策略的函数所取代,并提供可供选择的函数。

def printElapsedTime(t: Long, rounding: Double => Long = math.round) {
  println(rounding(t*0.001))
}
printElapsedTime(1700, math.floor)  // Change strategy

模板方法

特质在这里提供了更多的可能性,最好将它们视为另一种模式。您可以根据您的抽象级别从尽可能多的信息中填充代码。我不想真的称它为同样的东西。

访问者

结构类型隐式转换 之间,Scala 拥有比Java典型的访问者模式更多的能力。使用原始模式没有意义;你只会分心于正确的方法。许多例子实际上只是希望在被访问的对象上定义一个函数,而Scala可以轻松地为您完成这个操作(即将任意方法转换为函数)。


1
有趣的是比较你关于Scala的答案和我的,我更注重函数式编程。我甚至没有提到特质,因为那真的是面向对象的概念。 - Daniel C. Sobral
命令模式与监听器和事件无关。它们是观察者模式的一部分。 - jaco0646

12
好的,让我们简要地看一下这些模式。我从纯函数式编程的角度来看待所有这些模式,并省略了许多Scala可以从面向对象的角度改进的东西。Rex Kerr的答案提供了一个有趣的反驳观点(在写完我的答案后,我才看到他的答案)。
考虑到这一点,我想说,研究持久数据结构(函数式纯数据结构)和单子模式很重要。如果你想深入学习,我认为范畴论基础是重要的——范畴论可以正式描述所有程序结构,包括命令式的程序结构。
创建型模式
构造函数不过是一个函数而已。例如,对于类型T的无参构造函数,它不过是一个函数() => T。实际上,Scala对函数的语法糖也被用在了case类中:
case class T(x: Int)

那相当于:
class T(val x: Int) { /* bunch of methods */ }
object T {
  def apply(x: Int) = new T(x)
  /* other stuff */
}

这样你就可以使用 T(n) 实例化 T 而不是使用 new T(n)。你甚至可以像这样编写它:
object T extends Int => T {
  def apply(x: Int) = new T(x)
  /* other stuff */
}

这将把T转换为一个正式的函数,而不改变任何代码。

这是考虑创建模式时需要记住的重要点。所以让我们来看看它们:

抽象工厂

这个模式不太可能发生太大变化。一个类可以被视为一组密切相关的函数,因此通过类很容易实现一组密切相关的函数,这就是这个模式为构造函数所做的事情。

建造者

建造者模式可以通过柯里化函数或部分函数应用程序来替换。

def makeCar: Size => Engine => Luxuries => Car = ???
def makeLargeCars = makeCar(Size.Large) _

def makeCar: (Size, Engine, Luxuries) => Car = ???
def makeLargeCars = makeCar(Size.Large, _: Engine, _: Luxuries)

工厂方法

如果放弃子类化,该方法将变得过时。

原型

不会改变--实际上,这是在函数式数据结构中创建数据的常见方式。请参阅案例类的copy方法,或返回集合的所有非可变方法。

单例

当您的数据是不可变的时,单例并不特别有用,但Scala object以安全的方式实现了这种模式。

结构模式

这主要与数据结构有关,函数式编程的重要点是数据结构通常是不可变的。你最好去看看持久化数据结构、单子和相关概念,而不是试图将这些模式转化为函数式等价物。

并不是说这里的某些模式不相关。我只是说,作为一般规则,你应该先研究上面的东西,而不是试图将结构模式转化为函数式等价物。

适配器

这种模式与类(名义类型)有关,因此只要您拥有它,它仍然很重要,当您没有它时,它就不相关了。

桥接

与OO架构有关,因此与上述相同。

组合

看看镜头和拉链。

装饰器

装饰器只是函数组合。如果您正在装饰整个类,则可能不适用。但是,如果您将功能提供为函数,则在保持其类型的同时组合函数是装饰器。

外观

与桥接的评论相同。

享元

如果您认为构造函数是函数,则将享元视为函数记忆化。此外,享元与如何构建持久数据结构密切相关,并且从不变性中获益很多。

代理

与适配器的评论相同。

行为模式

这到处都是。其中一些完全无用,而其他一些在函数设置中仍然像以往一样相关。

责任链

像装饰器一样,这是函数组合。

命令

这是一个函数。如果您的数据是不可变的,则撤消部分是不必要的。否则,只需保留一对函数及其反向即可。另请参阅Lenses。

解释器

这是一个单子。

迭代器

它可以通过向集合传递一个函数来使其过时。这实际上就是Traversable使用foreach所做的。另外,请参见Iteratee。

中介者

仍然相关。

备忘录

对于不可变对象没有用处。此外,它的重点是保持封装性,在FP中并不是主要关注点。

请注意,这个模式不是序列化,而序列化仍然是相关的。

观察者

相关,但请参见Functional Reactive Programming。

状态

这是一个单子。

策略

策略是一个函数。

模板方法

这是一个面向对象的设计模式,因此对于面向对象的设计是相关的。

访问者

访问者只是接收一个函数的方法。实际上,这就是Traversableforeach所做的事情。

在Scala中,它也可以用提取器替换。


不错!我希望它能够更详细一些,包括链接或其他指针。这个答案只对那些了解《Scala函数式编程》一书并且对上述每个模式都有实践经验的人有帮助。尽管如此,给出了正确的指引仍然很棒。 - Alex Pakka

2
我认为,在函数式语言中根本不需要使用“命令”模式。与其将命令函数封装在对象中并选择适当的对象,不如直接使用适当的函数本身。
“享元”只是缓存,在大多数函数式语言中都有默认实现(例如clojure中的memoize)。
即使是“模板方法”,“策略”和“状态”也可以通过在方法中传递适当的函数来实现。
因此,在尝试函数式编程时,我建议不要深入研究设计模式,而是阅读一些关于函数概念(高阶函数、惰性求值、柯里化等)的书籍。

1
@Optimight 这个问题与 http://stackoverflow.com/questions/9342930/programming-pattern-comparison 有关,请查看 Joshua Suereth 的书Runar 的书,了解 Scala 函数式编程方面的内容。 - om-nom-nom
@om-nom-nom 是的,先生。SO纪律在这里建议什么?我应该关闭我的问题并从这里直接链接到您建议的问题吗?还是我应该做些什么? - Optimight
似乎现在流行声称函数式语言中不需要模式,但大多数情况下这种说法都是错的。如果仅仅使用适当的函数来实现撤销(命令模式的一部分),那么如何实现呢?而且,“通过方法传递适当的函数”对我来说就是使用策略模式(例如),只是有一个不同的实现方式(参见http://en.wikipedia.org/wiki/Strategy_pattern,该页面指出可以使用一级函数来实现)。 - The Archetypal Paul
@Optimight 不用担心,继续提问吧。虽然它们相关但并非重复问题,所以没问题的。 - om-nom-nom
@Optimight:Joshua Suereth的书是一个很好的起点。它不仅深入探讨了Scala的函数式方面,还谈到了一种叫做“表达式导向编程”的东西,在Scala中你可能经常使用,但在Java中几乎从未使用过。 - Justin W
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接