單例(Singleton)指的是一個類別只會有唯一實例。單例應用於程式必須提供服務或操作的單一入口,像是作為物件工廠(Object factory)、執行時期僅需唯一實例的資源等,為了確定是唯一實例,基本上你無法自行實例化,通常這是透過設計或由語言本身機制來提供唯一實例的保證。
在Scala中,如果要建立單例物件(Singleton object),可以使用object關鍵字,例如:
object StringUtil {
def rightPad(s: String, n: Int) = String.format("%1\$-" + n + "s", s)
def leftPad(s: String, n: Int) = String.format("%1\$" + n + "s", s)
}
注 意上面這個程式不是定義類別,StringUtil並非類別名稱,你也沒辦法建構StringUtil實例。程式會建立一個物件,由StringUtil 名稱參考住(技術上來說,在編譯為位元碼後,Scala會產生一個StringUtil\$類別,並建立一個物件由StringUtil名稱參考住)。
由於StringUtil參考至單例物件,所以你可以如下使用StringUtil上所定義的方法:
println(StringUtil.rightPad("Justin", 10))
println(StringUtil.leftPad("Justin", 10))
如果你熟悉Java,這樣的用法就像是Java中於類別中定義靜態(static)方法,不過Scala中並沒有靜態方法(這被視為破壞物件封裝性的語法),不過這樣的用法確實是單例物件的一個應用,用來組織某些公用函式,一個你已經使用過的例子就是 scala.Math,其組織了許多數學相關函式,如果你想看看Scala有無提供一些數學函式,可以先查看scala.Math是否已經提供。
像上面的單例物件為獨立物件(Stand-alone object)。單例物件的另一個應用,就是與同名類別形成伴侶(Companion)。例如,若你想要某個自行設計的類別,在應用程式只會產生唯一實例,則可以如下設計:
class Resource private {
def service(request: String) = request + "...processed..."
}
object Resource {
private val resource = new Resource
def get = resource
}
在定義Resource類別時,你使用了private將主要建構式修飾為私用,你沒辬法呼叫一個私用方法(建構式也是一種方法),所以你沒辦法直接實例化Resource類別。如果主要建構式有參數,則可以如下撰寫private建構式:
class Resource private (x: String) {
...
}
...
}
在類別Resource的定義下面,你定義了一個同名的單例物件Resource,這個Resource單例物件就是Resource類別的伴侶物件(Companion object),而Resource類別則為Resource單例物件的伴侶類別(Companion class),在Scala中,伴侶類別與伴侶物件必須位在同一個.scala檔案中(注意private建構式並非伴侶類別所必要的,這只是範例在設計上,不希望你自行實例化該類別)。
伴侶物件的特性之一,就是可以存取伴侶類別的私用成員,所以在Resource伴侶物件中,你可以實例化Resource(一個private建構),你則透過get()方法取得物件,如此確保所取得的是唯一實例,一個使用方式如下:
val resource = Resource.get
println(resource.service("XD"))
事實上,你無需定義get方法,你可以定義一個apply方法,這稱之為工廠方法(Factory method),例如:
class Resource private {
def service(request: String) = request + "...processed..."
}
object Resource {
private val resource = new Resource
def apply() = resource
}
你可以這麼使用:
val resource = Resource()
println(resource.service("XD"))
事實上,apply是Scala所提供的語法蜜糖(Syntax sugar),以上例而言,Scala會自動將Resource()的呼叫,轉換為Resource.apply()的呼叫(這也就是為什麼括號不能省略的原因)。如果你需要給apply參數,則呼叫單例物件時也要有對應的參數,例如:
object Singleton {
def apply(x: String) = "apply.." + x
}
println(Singleton("XD"))
在上例中,當你呼叫Singleton("XD")時,實際上是轉換為Singleton.apply("XD")。