Scala 设计模式

2019-05-11 16:59:58 | 编辑 | 添加

设计模式就是编程的方法和设计程序的思想,其实不论什么编程语言,设计模式都是差不多的,只是Scala有其语言特点,他的设计模式实现起来也有其特点。Scala设计模式分为创建型模式、结构型模式和行为型模式这三类,下面进行简单讲解。


1 构造型模式

  • 抽象工厂(abstract factory)

抽象工厂是一种基于特定类型家族构造实例的抽象体,使用抽象工厂时无需指定构造实例的类型。对象中的apply 方法能实现这一模式,apply 方法能够根据传入的参数初始化出适当类型的实例。除此之外,将函数传递给Monad.flatMap,或者使用Applicative定义apply 方法同样能够实现这种抽象构造的功能。


  • 构造器模式(builder)

构造器会根据对象内容,对复杂对象的构造过程进行分解,这样一来便可以在构造多种不同的对象时复用相同的构造过程。collection.generic.CanBuildFrom(http://www.scala-lang.org/api/current/index.html#scala.collection.generic.CanBuildFrom)便是一个很好的Scala 示例,使用该对象时可以利用像map 这样的组合器方法构造一个相同类型的新的集合对象,构造出的集合类型与输入集合类型相同。


  • 工厂方法(factory method)

在父类中定义一个方法,子类型会重载(或实现)该方法。而子类型正是通过这种方式决定初始化什么类型和初始化的方式。CanBuildFrom.apply 方法便是一个用于创建构造器实例的抽象方法,而新创建的构造器则可以用于构建实例。子类型与特定的实例可以提供工作方法的具体实现。Applicative.apply 可以提供类似的抽象。


  • 原型模式(prototype)

在原型模式中存在一个用作原型的实例,之后复制该原型实例并对其进行选择性的修改,从而得到新实例。Case 类的copy 方法是原型模式的一个很好的示例,使用copy 方法时,用户可以对某一实例进行克隆,同时用户还能通过指定参数的方式对该实例进行修改。


  • 单例模式(singleton)

单例模式确保类型只有一个实例,且该类型的所有用户都能访问这个唯一的实例。Scala 通过object 实现了单例模式,因此该模式可以看作Scala 语言的最好功能。


2 结构型模式

  • 适配器模式(adapter)

适配器模式创建用户期望的接口对其他抽象体进行封装,封装完成后,用户便可以使用该抽象体。在之前,我们讨论了Observer 模式的一些可能的实现,以及如何在这些实现中进行取舍,其中重点提到了抽象体和潜在观察者之间建立耦合的方式。我们首先使用trait 描述了观察者需要实现的功能。之后我们为了降低依赖,又使用结构化类型取代了该trait。事实上,潜在观察者并不需要实现我们所定义的trait,它只需要提供某一专门的方法即可。最后,我们注意到如果使用匿名函数,可以完全地解除与观察者之间的耦合。这个匿名函数便是适配器。观察者所观察的主体会调用该适配器,而该适配器内部则会调用所有需要执行的观察者。


  • 桥接模式(bridge)

将抽象体和实现体分隔开,这样一来便能独立地对这两者进行更改。类型类(typeclass)可以看作是桥接模式的一个有趣示例,从逻辑上看,桥接模式被类型类应用到了极致。类型类不仅将类型可能需要的抽象从类型中剥离开,只在需要时才重新添加到类型中,而且指定类型的类型类抽象实现同样也被单独定义在其他地方。


  • 组合模式(composite)

使用由一组实例组成的树形结构表示“局部-整体”的层次关系。对个体实例和组合实例进行相同的对待。函数式代码倾向于避免使用类型之间的各类层次关系,它青睐于使用像树这样的泛型结构来表示层次,并提供操作树的统一访问方法和完整的组合器。Lens 便是这样一类工具,我们可以使用它对复杂的组合进行处理。


  • 装饰模式(decorator)

为某一对象“动态”地附加额外的职责。无需对类型的原始代码进行修改,类型类在编译期间便能执行这样的操作。假如你希望能够提供真正的运行灵活性,那么Dynamic特征(http://www.scala-lang.org/api/current/index.html#scala.Dynamic) 便能派上用场。假如你希望能够对某一值或计算过程进行“装饰”,那么你可以分别使用Monad 和Applicative。


  • 外观模式(facade)

为了能够使子系统更易使用,外观模式为子系统中的多个接口提供统一的接口。包对象便支持这类模式。包对象能够只对外暴露应该公有的类型和行为。


  • 享元模式(flyweight)

为了能够有效地允许大量细粒度对象对资源进行访问,采用共享的方式提供资源。函数式编程强调对象的不可变性,这也使得我们能够很直接的在函数式编程语言中实现享元模式。像Vector(http://www.scala-lang.org/api/current/index.html#scala.collection.immutable.Vector)这样的持久化数据类型便是很重要的示例。


  • 代理模式(proxy)

代理模式会提供其他实例代理对象,以对该实例的所有访问进行控制。包对象在细粒度级别上提供了对代理模式的支持。值得注意的是,由于多个用户操作不可变实例时并不会出现访问冲突这样的问题,因此Scala 对访问控制的需求也就降低了。


3 行为型模式

  • 职责链模式(chain of responsibility)

职责链模式消除了发送者和接收者之间的耦合。该模式允许一组潜在的接收者对请求进行处理。当前面的接收者执行完毕,后续的接收者才开始执行。这也是模式匹配的工作原理。职责链模式的描述更适用于Akka 的receive 块,在receive 代码块中,“发送者”和“接收者”不只是简单的暗喻。


  • 命令模式(command)

对服务请求进行具体化。这样一来,我们便能将这些请求队列化,使请求可以执行撤销、重新执行等操作。这也是Akka 的工作方式。尽管尚未支持撤销和重做功能,不过Akka 在原则上是能够提供这些功能的。Monad 模式的一类典型应用便是此类问题的一种扩展,在此类应用中,我们会仔细管理系统的状态转换,并将“命令”步骤按照可预测的顺序进行组合(对于默认情况下惰性求值的语言而言,这是一项重要的特性)。


  • 解释器模式( interpreter)

定义了一门语言以及解释该语言表达式的方式。“四人组”编写了设计模式后,DSL 这一名词也开始兴起。DSL 语言中的一些方法已经在第20 章讲解过。


  • 迭代器模式(iterator)

能够在不暴露容器实现细节的情况下对该容器进行遍历。操作函数化容器时,几乎所有的操作都遵循该设计模式。


  • 中介者模式(mediator)

为了避免实例之间直接地交互,中介者模式使用中介者来实现交互功能,该模式允许各类交互分别独立地改良。ExecutionContext(http://www.scala-lang.org/api/current/index.html#scala.concurrent.ExecutionContext)被用于协调异步计算。在使用Future 时,正是由于ExecutionContext 的存在,Future 对象无需了解关于异步协作的任何机制。因

此我们可以将ExecutionContext 视为中介者的一个例子。与之类似,Akka 运行时通过在actor 之间建立少量的连接,能对actor 之间的消息进行调解。在此期间,Akka 需要使用特定的ActorRef 对象(http://doc.akka.io/api/akka/current/index.html#akka.actor.ActorRef)发送消息,无需通过程序硬编码描述actor 之间的依赖关系,Akka 只要利用像名字查找这样的方法便能找到消息的接收方。除此之外,Akka 为actor 提供了一层可以进行间接交互的模块。


  • 备忘录模式(momento)

备忘录模式会捕获实例状态,对实例状态进行存储,并使用存储状态对该状态进行恢复。使用纯函数能够更容易地实现记忆化(memoization)功能。我们可以使用装饰器(decorator)添加备忘录,这样一来,假如添加备忘录时传入了之前已经使用过的参数,系统可以避免重复调用该函数,直接返回备忘录。


  • 观察者模式(observer)

根据主体状态,建立主体与观察者之间一对多的依赖关系。当主体状态发生变化时,通知观察者。在之前讲述适配器模式时,我们曾讨论过该模式。


  • 状态模式(state)

当实例状态发生改变时,状态模式允许实例对自身行为进行更改。假如状态值是不可变值,那么为了能够表示新的状态,状态模式会构造新的实例。原则上,新构造的实例会表现出不一样的行为,尽管这些变化会受到某个常见父类型抽象体的限制。状态机是状态模式的更常见的形式


  • 策略模式(strategy)

对一组相关算法进行物化操作,使得这些算法能交换使用。利用高阶函数能够简单地实现这一模式。例如:调用map 方法时,调用者能够选择“算法”对每个元素进行转换。


  • 模板方法(template method)

以final 方法的形式定义某一算法的实现框架,该方法会调用其他的一些函数,而为了能够对这些方法的行为进行定制,子类可以对这些被调用方法进行覆写。覆写具体方法的做法既破坏了既有规则,也不安全。模板方法则更讲原则且更为安全,也正因为如此,模板方法是我最喜欢的模式之一。值得注意的是,除了定义用于覆写的抽象方法之外,我们还可以将模板方法定义成高阶函数,将具体实现传入到高阶函数中,以实现自定义功能。


  • 访问者模式(visitor)

使用访问者模式时,我们会向某个实例中插入一个协议。这样一来,其他代码便能通过该协议访问原本不被该类型支持的内部操作。但是,该模式其实是一个很糟糕的模式,它侵犯了public 接口,并使实现变得复杂。不过幸运的是,我们有更好的解决选项。类型定义者可以通过定义unapply 或unapplySeq 方法来定义一种低开销的协议,对外只暴露应该开放的内部状态。模式匹配便使用了这一功能提取匹配值并实现新的功能。类型类无法提供内部状态的访问接口,因此无法满足一些特殊的需求,不过我们还是可以使用它们为既存类型添加新的行为。当然,访问内部状态本身就是一个很糟糕的设计。