哪个GOF设计模式有完全不同的实现(java vs Scala)

时间:2012-06-20 16:28:36

标签: scala design-patterns

最近我读了以下问题:

  

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

标题问题的链接: Visitor Pattern in Scala。接受的答案以

开头
  

是的,您应该从模式匹配开始,而不是   访客模式。看到这个   http://www.artima.com/scalazine/articles/pattern_matching.html

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

创作模式

  • 抽象工厂
  • 生成器
  • 工厂方法
  • 原型
  • Singleton:直接创建一个Object(scala)

结构模式

  • 适配器
  • 组合
  • 装饰
  • 门面
  • 飞锤
  • 代理

行为模式

  • 责任链
  • 命令
  • 解释
  • 迭代
  • 中保
  • 备忘录
  • 观察
  • 国家
  • 策略
  • 模板方法
  • 访客: Patten Matching(scala)

3 个答案:

答案 0 :(得分:43)

对于几乎所有这些,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中尝试此操作,请确保在同一个块中定义类和对象构建器,即使用:paste。)检查类型与<:<的组合,泛型类型参数和案例类的复制方法是一个非常强大的组合。

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

工厂方法&#39;主要用途是保持你的类型直;否则你也可以使用构造函数。使用Scala强大的类型系统,你不需要帮助保持你的类型,所以你也可以使用同伴对象中的构造函数或apply方法来创建你的类,并创建那些办法。特别是在伴随对象的情况下,保持该接口的一致性并不比保持工厂对象中的接口一致。因此,工厂对象的大部分动机都消失了。

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

原型

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

的Singleton

Scala用object包含这些内容。他们是单身人士 - 使用和享受!

结构模式

适配器

Scala的trait在这里提供了更多的力量 - 而不是创建一个实现接口的类,例如,你可以创建一个只实现 part 的特性界面,其余部分供您定义。例如,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中同样有价值。

装饰

装饰者很尴尬。在继承不是您想要的情况下,您通常不希望在不同的类上使用相同的方法;你真正想要的是在同一个类上使用不同的方法,而不是默认的东西。 enrich-my-library pattern通常是一个优越的替代品。

门面

Facade在Scala中比在Java中工作得更好,因为你可以让特性进行部分实现,这样你就不必在组合它们时自己完成所有工作。

飞锤

尽管flyweight的想法在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提供parser combinators,它比设计模式建议的简单解释器强大得多。

迭代

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

中保

这在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),或者使用actor,其中一个actor向所有其他人发送关于状态变化的消息,但是每个actor都可以应对过时。

国家

这在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

模板方法

Traits在这里提供了更多的可能性,最好只考虑另一种模式。您可以从您在抽象级别获得的尽可能多的信息中填写尽可能多的代码。我真的不想把它称之为同样的东西。

访问者

structural typingimplicit conversion之间,Scala的功能远远超过了Java的典型访问者模式。使用原始模式毫无意义;你会因为正确的方式而分心。许多例子实际上只是希望在被访问的东西上定义一个函数,Scala可以为你做些琐事(即将任意方法转换为函数)。

答案 1 :(得分:11)

好的,让我们简要介绍一下这些模式。我纯粹从功能编程的角度来看待所有这些模式,并且从OO的角度来看,Scala可以改进很多东西。 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转换为正式函数,而不更改任何代码。

在考虑创作模式时,这是要记住的重点。让我们来看看它们:

抽象工厂

这个不太可能发生太大变化。类可以被认为是一组密切相关的函数,因此一组紧密相关的函数很容易通过类实现,这就是这种模式对构造函数的作用。

生成器

构建器模式可以由curried函数或部分函数应用程序替换。

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方法,或返回集合的集合上的所有非可变方法。

的Singleton

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

结构模式

这主要与数据结构有关,函数式编程的重点是数据结构通常是不可变的。除了尝试翻译这些模式之外,你最好还是考虑持久性数据结构,monad和相关概念。

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

适配器

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

与OO架构相关,与上述相同。

组合

拍摄镜头和拉链。

装饰

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

门面

与Bridge相同的评论。

飞锤

如果您将构造函数视为函数,请将flyweight视为函数memoization。此外,Flyweight与持久性数据结构的构建方式有着内在的联系,并且从不变性中获益良多。

代理

与适配器相同的评论。

行为模式

这到处都是。其中一些是完全无用的,而另一些则与功能设置中的一样相关。

责任链

与装饰师一样,这是功能组合。

命令

这是一个功能。如果您的数据是不可变的,则无需撤消部分。否则,只需保留一对功能及其反向。另见镜头。

解释

这是一个单子。

迭代

只需将函数传递给集合即可使其过时。事实上,这就是Traversableforeach的关系。另外,请参阅Iteratee。

中保

仍然相关。

备忘录

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

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

观察

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

国家

这是一个单子。

策略

策略是一种功能。

模板方法

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

访问者

访客只是一种接收功能的方法。事实上,这就是Traversable foreach的作用。

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

答案 2 :(得分:2)

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

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

即使Template methodStrategyState也可以通过在方法中传递适当的函数来实现。

所以,我建议你不要深入设计模式,当你尝试自己的功能风格,但阅读一些关于功能概念的书籍(高阶函数,懒惰,currying等)