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模式,準確地說實際上是靜態工廠模式,它並不在GOF 23種模式之列,但做爲對複雜建立邏輯的一種封裝,經常被開發人員使用。站在OCP(開放封閉原則)的角度講,該模式對擴展不是開放的,但對於修改而言,倒是封閉的。若是建立邏輯發生了變化,能夠保證僅修改該靜態工廠方法一處。同時,該模式還能夠極大地簡化對象建立的API。測試
在Scala中,經過引入伴生對象(Companion Object)來簡化靜態工廠方法,語法更加乾淨,體現了Scala精簡的設計哲學。即便不是要使用靜態工廠,咱們也經常建議爲Scala類定義伴生對象,尤爲是在DSL上下文中,更是如此,由於這樣能夠減小new關鍵字對代碼的干擾。this
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變得更快。只有在須要時,這些變量對應的代碼纔會執行。這也是延遲加載的涵義。
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。在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來自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(抽象數據類型)是風馬牛不相及的兩個概念。