31

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

在 Scala 中是否有使用访问者模式的用例?每次在 Java 中使用访问者模式时,是否应该在 Scala 中使用模式匹配?

标题为问题的链接: Scala 中的访问者模式。接受的答案以

是的,您可能应该从模式匹配而不是访问者模式开始。请参阅此 http://www.artima.com/scalazine/articles/pattern_matching.html

我的问题(受上述问题的启发)是哪种 GOF 设计模式在 Scala 中有完全不同的实现?如果我在 Scala 中编程,我应该在哪里小心而不遵循基于 Java 的设计模式(四人组)编程模型?

创造模式

  • 抽象工厂
  • 建造者
  • 工厂方法
  • 原型
  • 单例:直接创建一个对象(scala)

结构图案

  • 适配器
  • 合成的
  • 装饰器
  • 正面
  • 蝇量级
  • 代理

行为模式

  • 责任链
  • 命令
  • 口译员
  • 迭代器
  • 调解员
  • 纪念
  • 观察者
  • 状态
  • 战略
  • 模板法
  • 访问者:Patten Matching (scala)
4

3 回答 3

45

对于几乎所有这些,有 Scala 替代方案涵盖了这些模式的一些但不是全部用例。当然,所有这些都是 IMO,但是:

创作模式

建造者

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。)检查类型与<:<、泛型类型参数和案例类的复制方法的组合使得一个非常强大的组合。

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

工厂方法的主要用途是使您的类型保持直截了当;否则你也可以使用构造函数。使用 Scala 强大的类型系统,您不需要帮助来保持类型的正确性,因此您也可以使用apply类的伴随对象中的构造函数或方法并以这种方式创建事物。特别是在伴随对象的情况下,保持接口一致并不比保持工厂对象中的接口一致更难。因此,工厂对象的大部分动机都消失了。

类似地,抽象工厂方法的许多情况可以通过从适当的特征继承的伴随对象来替换。

原型

当然,被覆盖的方法等在 Scala 中占有一席之地。但是,在设计模式网站上用于原型模式的示例在 Scala(或 Java IMO)中是相当不可取的。但是,如果您希望超类根据其子类选择操作而不是让它们自己决定,那么您应该使用match而不是笨拙的instanceof测试。

辛格尔顿

Scala 使用object. 他们是单身人士——使用和享受!

结构模式

适配器

Scalatrait在这里提供了更多的功能——而不是创建一个实现接口的类,例如,您可以创建一个仅实现部分接口的 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) }
}

这使您可以跳过大部分桥接模式样板,同时实现高度的实现独立性,并且仍然让您的类遵守原始接口(因此您不必继续包装和解包它们)。

合成的

使用案例类特别容易实现复合模式,尽管进行更新相当费力。它在 Scala 和 Java 中同样有价值。

装饰器

装饰师很尴尬。如果继承不是您想要的,您通常不想在不同的类上使用相同的方法;你真正想要的是同一个类上的不同方法,它可以做你想做的事情,而不是默认的事情。“丰富我的图书馆”模式通常是更好的替代品。

正面

Facade 在 Scala 中比在 Java 中效果更好,因为您可以让特征携带部分实现,因此在组合它们时您不必自己完成所有工作。

蝇量级

尽管享元思想在 Scala 中与 Java 一样有效,但您可以使用更多工具来实现它:lazy val,除非实际需要,否则不会创建变量(然后被重用),并且by-name parameters,您只执行如果函数实际使用该值,则创建函数参数所需的工作。也就是说,在某些情况下,Java 模式保持不变。

代理

在 Scala 中的工作方式与 Java 相同。

行为模式

责任链

在您可以按顺序列出责任方的情况下,您可以

xs.find(_.handleMessage(m))

假设每个人都有一个handleMessage方法,true如果消息被处理则返回。如果您想在消息发生时对其进行变异,请改用折叠。

由于很容易将责任方归Buffer为某种类型,Java 解决方案中使用的复杂框架很少在 Scala 中占有一席之地。

命令

这种模式几乎完全被函数所取代。例如,而不是所有

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

你只是

def onChange(f: ChangeEvent => Unit)

口译员

Scala 提供的解析器组合器比作为设计模式建议的简单解释器要强大得多。

迭代器

Scala 已Iterator内置到其标准库中。Iterator让你自己的类扩展或几乎是微不足道的Iterable;后者通常更好,因为它使重用变得微不足道。绝对是一个好主意,但如此简单,我几乎不会称之为模式。

调解员

这在 Scala 中运行良好,但通常对可变数据有用,如果不小心使用,即使中介也可能与竞争条件发生冲突。相反,尽可能尝试将您的相关数据全部存储在一个不可变集合、案例类或其他任何东西中,并且在进行需要协调更改的更新时,同时更改所有内容。这不会帮助您与 交互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

观察者

这充其量是一个脆弱的模式。尽可能支持保持不可变状态(请参阅 Mediator),或者使用参与者,其中一个参与者向所有其他参与者发送有关状态更改的消息,但每个参与者都可以应对过时的情况。

状态

这在 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 可以为您做些微不足道的事情(即将任意方法转换为函数)。

于 2012-06-20T20:15:43.440 回答
12

好的,让我们简单看看这些模式。我纯粹从函数式编程的角度来看待所有这些模式,而忽略了许多 Scala 从 OO 角度可以改进的东西。Rex Kerr 的答案为我自己的答案提供了一个有趣的反驳点(我在写完自己的答案后才阅读他的答案)。

考虑到这一点,我想说研究持久数据结构(功能纯数据结构)和 monad 很重要。如果你想深入,我认为范畴论基础很重要——范畴论可以正式描述所有程序结构,包括命令式结构。

创作模式

构造函数只不过是一个函数。例如,类型 T 的无参数构造函数只不过是一个函数() => T。事实上,Scala 的函数语法糖在案例类中得到了利用:

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使用T(n)而不是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方法,或返回集合的集合上的所有非可变方法。

辛格尔顿

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

结构模式

这主要与数据结构有关,而函数式编程的重点是数据结构通常是不可变的。您最好查看持久数据结构、monad 和相关概念,而不是尝试翻译这些模式。

并不是说这里的某些模式不相关。我只是说,作为一般规则,您应该研究上述内容,而不是试图将结构模式转换为功能等价物。

适配器

这种模式与类(名义类型)有关,所以只要你有它,它仍然很重要,而当你没有它时,它就无关紧要了。

与OO架构相关,同上。

合成的

很多在镜头和拉链。

装饰器

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

正面

与桥的评论相同。

蝇量级

如果您将构造函数视为函数,请将享元视为函数记忆。此外,享元本质上与如何构建持久性数据结构相关,并且从不变性中受益匪浅。

代理

与适配器相同的注释。

行为模式

这到处都是。其中一些是完全无用的,而另一些在功能环境中与往常一样相关。

责任链

和装饰器一样,这是功能组合。

命令

这是一个功能。如果您的数据是不可变的,则不需要撤消部分。否则,只需保留一对函数及其相反。另请参阅镜头。

口译员

这是一个单子。

迭代器

只需将函数传递给集合即可使其过时。Traversable实际上,这foreach就是 。另请参阅 Iteratee。

调解员

仍然相关。

纪念

对不可变对象无用。此外,它的重点是保持封装,这不是 FP 中的主要问题。

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

观察者

相关,但请参阅功能响应式编程。

状态

这是一个单子。

战略

策略是一种功能。

模板法

这是一种 OO 设计模式,因此它与 OO 设计相关。

游客

访问者只是接收函数的方法。事实上,这就是Traversable's 的foreach作用。

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

于 2012-06-20T21:03:31.647 回答
2

我想,Command函数式语言根本不需要模式。而不是在对象内部封装命令函数然后选择合适的对象,只需使用合适的函数本身。

Flyweight只是缓存,并且在大多数函数式语言中都有默认实现(在 clojure 中的memoize )

Even Template method, StrategyandState可以通过在方法中传递适当的函数来实现。

所以,我建议你在尝试函数式风格时不要深入设计模式,而是阅读一些关于函数式概念的书籍(高阶函数、惰性、柯里化等)

于 2012-06-20T16:40:57.647 回答