做为 Scala 语法糖的设计模式

Scala算是一门博采众家之长的语言,兼具OO与FP的特性,若使用恰当,能够更好地将OO与FP的各自优点发挥到极致;然而问题也随之而来,假若过度地夸大OO特性,Scala就变成了一门精简版的Java,写出的是没有Scala Style的拙劣代码;假若过度追求FP的不变性等特性,由于Scala在类型系统以及Monad实现的繁琐性,又可能致使代码变得复杂,不易阅读,反而得不偿失。程序员

看来,赋予程序员选择的自由,有时候未必是好事!设计模式

在OO世界里,设计模式曾经风靡全世界,你不懂设计模式,都很差意思说本身是程序员。如今呢?说你懂设计模式,倒显得你逼格低了,内心鄙视:“这年头谁还用设计模式,早过期了!”程序员心中的鄙视链开始加成,直接失血二十格。安全

其实什么事情都得辩证来看!设计模式对OO设计的推动做用不容忽视,更不容轻视。我只是反对那种为了“模式”而“模式”的僵化思想,若是没有明白设计模式的本质思想,了解根本的设计原理,设计模式无非就是花拳绣腿罢了。固然,在FP世界里,设计模式开始变味开始走形,但诸多模式的本质,例如封装、抽象,仍然贯穿其中,不过是表达形式迥然而已罢了。session

在混合了OO与FP的Scala语言中,咱们来观察设计模式的实现,会很是有趣。Pavel Fatin有篇博客Design Patterns in Scala将Java设计模式与Scala进行了对比,值得一读。我这里想借用他的案例,而后从另外一个角度来俯瞰设计模式。ide

在Pavel Fatin比较的设计模式中,部分模式在Scala中不过是一种语法糖(Syntax Sugar),包括:函数

  • Factory Method
  • Lazy Initialization
  • Singleton
  • Adapter
  • Value Object

Factory Method

文中给出的Factory Method模式,准确地说实际上是静态工厂模式,它并不在GOF 23种模式之列,但做为对复杂建立逻辑的一种封装,经常被开发人员使用。站在OCP(开放封闭原则)的角度讲,该模式对扩展不是开放的,但对于修改而言,倒是封闭的。若是建立逻辑发生了变化,能够保证仅修改该静态工厂方法一处。同时,该模式还能够极大地简化对象建立的API。测试

在Scala中,经过引入伴生对象(Companion Object)来简化静态工厂方法,语法更加干净,体现了Scala精简的设计哲学。即便不是要使用静态工厂,咱们也经常建议为Scala类定义伴生对象,尤为是在DSL上下文中,更是如此,由于这样能够减小new关键字对代码的干扰。this

Lazy Initialization

lazy修饰符在Scala中有更深远的涵义,例如牵涉到所谓严格(Strictness)函数与非严格(Non-strictness)函数。在Scala中,若未明确声明,全部函数都是严格求值的,即函数会当即对它的参数进行求值。而若是对val变量添加lazy修饰符,则Scala会延迟对该变量求值,直到它第一次被引用时。若是要定义非严格函数,能够将函数设置为by name参数。spa

scala的lazy修饰符经常被用做定义一些消耗资源的变量。这些资源在初始化时并不须要,只有在调用某些方法时,才须要准备好这些资源。例如在Spark SQL的QeuryExecution类中,包括optimizedPlan、sparkPlan、executedPlan以及toRdd等,都被定义为lazy val:线程

class QueryExecution(val sparkSession: SparkSession, val logical: LogicalPlan) {
  lazy val analyzed: LogicalPlan = {
    SparkSession.setActiveSession(sparkSession)
    sparkSession.sessionState.analyzer.execute(logical)
  }
  lazy val withCachedData: LogicalPlan = {
    assertAnalyzed()
    assertSupported()
    sparkSession.sharedState.cacheManager.useCachedData(analyzed)
  }
  lazy val optimizedPlan: LogicalPlan = sparkSession.sessionState.optimizer.execute(withCachedData)
  lazy val sparkPlan: SparkPlan = {
    SparkSession.setActiveSession(sparkSession)
    planner.plan(ReturnAnswer(optimizedPlan)).next()
  }
  lazy val executedPlan: SparkPlan = prepareForExecution(sparkPlan)
  lazy val toRdd: RDD[InternalRow] = executedPlan.execute()
}复制代码

这样设计有一个好处是,当程序在执行到这些步骤时,并不会被立刻执行,从而使得初始化QueryExecution变得更快。只有在须要时,这些变量对应的代码才会执行。这也是延迟加载的涵义。

Singleton Pattern

C#提供了静态类的概念,但Java没有,而Scala则经过引入Object弥补了Java的这一缺失,并且从语义上讲,彷佛比静态类(Static Class)更容易让人理解。

Object能够派生自多个trait。例如派生自App trait,就可直接享有main函数的福利。

trait App extends DelayedInit {
  def main(args: Array[String]) = {
    this._args = args
    for (proc <- initCode) proc()
    if (util.Properties.propIsSet("scala.time")) {
      val total = currentTime - executionStart
      Console.println("[total " + total + "ms]")
    }
  }
}

object Main extends App复制代码

继承多个trait的好处是代码复用。咱们能够将许多小粒度方法的实现定义在多个trait中。这些方法若是被类继承,则成为实例方法,若是被Object继承,则变成了线程安全的静态方法(由于继承trait的实现就是一个mixin)。多么奇妙!因此不少时候,咱们会尽可能保证Obejct的短小精悍,而后将许多逻辑放到trait中。当你看到以下代码时,其实没必要惊讶:

object Main extends App 
  with InitHook
  with ShutdownHook
  with ActorSystemProvider
  with ScheduledTaskSupport复制代码

这种小粒度的trait既能够保证代码的复用,也有助于职责分离,还有利于测试。真是再好不过了!

Adapter Pattern

隐式转换固然能够用做Adapter。在Scala中,之因此能够更好地调用Java库,隐式转换功不可没。从语法上看,隐式转换比C#提供的扩展方法更强大,适用范围更广。

Pavel Fatin给出了日志转换的Adapter案例:

trait Log {
  def warning(message: String)
  def error(message: String)
}

final class Logger {
  def log(level: Level, message: String) { /* ... */ }
}

implicit class LoggerToLogAdapter(logger: Logger) extends Log {
  def warning(message: String) { logger.log(WARNING, message) }
  def error(message: String) { logger.log(ERROR, message) }
}

val log: Log = new Logger()复制代码

这里的隐式类LoggerToLogAdapter能够将Logger适配为Log。与Java实现Adapter模式不一样的是,咱们不须要去建立LoggerToLogAdapter的实例。如上代码中,建立的是Logger实例。Logger自身与Log无关,但在建立该对象的上下文中,因为咱们定义了隐式类,当Scala编译器遇到该隐式类时,就会为Logger添加经过隐式类定义的代码,包括隐式类中定义的对Log的继承,以及额外增长的warning与error方法。

在大多数场景,Adapter关注的是接口之间的适配。可是,当要适配的接口只有一个函数时,在支持高阶函数(甚至只要支持Lambda)的语言中,此时的Adapter模式就味如鸡肋了。假设Log与Logger接口只有一个log函数(无论它的函数名是什么),接收的参数为(Level, String),那么从抽象的角度来看,它们其实属于相同的一个抽象:

f: (Level, String) => Unit复制代码

任何一个符合该定义的函数,都是彻底适配的,没有类型与函数名的约束。

若是再加上泛型,抽象会更加完全。例如典型的Load Pattern实现:

def using[A](r : Resource)(f : Resource => A) : A =
    try {
        f(r)
    } finally {
        r.dispose()
    }复制代码

泛型A能够是任何类型,包括Unit类型。这里的f扩大了抽象范围,只要知足从Resource转换到A的语义,均可以传递给using函数。更而甚者能够彻底抛开对Resource类型的依赖,只须要定义了close()方法,均可以做为参数传入:

def using[A <: def close():Unit, B][resource: A](f: A => B): B =
    try {
        f(resource)
    } finally {
        resource.close()
    }

using(io.Source.fromFile("example.txt")) { source => {
    for (line <- source.getLines) {
        println(line)
    }
  }
}复制代码

由于FileResource定义了close()函数,因此能够做为参数传给using()函数。

Value Object

Value Object来自DDD中的概念,一般指的是没有惟一标识的不变对象。Java没有Value Object的语法,然而因其在多数业务领域中被频繁使用,Scala为其提供了快捷语法Case Class。在几乎全部的Scala项目中,均可以看到Case Class的身影。除了在业务中表现Value Object以外,还能够用于消息传递(例如AKKA在Actor之间传递的消息)、序列化等场景。此外,Case Class又能够很好地支持模式匹配,或者做为典型的代数数据类型(ADT)。例如Scala中的List,能够被定义为:

sealed trait List[+T]
case object Nil extends List[Nothing]
case class Cons[+T](h: T, t: List[T]) extends List[T]复制代码

这里,case object是一个单例的值对象。而Nil与Cons又都同时继承自一个sealed trait。在消息定义时,咱们经常采用这样的ADT定义。例如List定义中,Nil与Cons就是List ADT的sum或者union,而Cons构造器则被称之为是参数h(表明List的head)与t(表明List的tail)的product。这也是ADT(algebraic data type)之因此得名。注意它与OO中的ADT(抽象数据类型)是风马牛不相及的两个概念。

相关文章
相关标签/搜索