本文由 Captain 發表在 ScalaCool 團隊部落格。
在上篇 Java 到 Scala 系列中,我想你或多或少在語言特性上對object
有了一定的掌握,在瞭解完它酷酷的語言特性——讓靜態迴歸常態並能簡單運用其衍生出的方法後,我今天就來談談在現實應用方面自己對它的理解,不知道是不是也會給你一種耳目一新的感覺,畢竟「單例物件」作為一種天然的語言特性,華而不實並不是我們想看到的。
單例模式 VS 單例物件
我們已經知道了object
是作為打破靜態而存在的「單例物件」,在 Scala 中,「單例物件」使用頻率之高可以和 Java 中的 new 關鍵詞相比,又或是 Spring 中DI(Dependency Injection),所以我們不得不考慮到一些場景——多執行緒和效能開銷。現在就具體來看看它和 Java 實現的單例模式有什麼不同。
先來看看 Java 對於單例模式的實現:
餓漢模式
public class UniqueSingleton {
//類載入時就初始化
private static uniqueSingleton instance = new uniqueSingleton();
private UniqueSingleton() {
System.out.println(("UniqueSingleton is created"));
}
public static UniqueSingleton getInstance() {
return instance;
}
}
複製程式碼
單例模式就靠以上幾行程式碼實現了,就是這麼簡單。但是餓漢模式有這麼一個缺點,無論你有沒有呼叫它,它在 JVM 載入類這個過程中都會將單例載入好,所以它並不具備惰性傳值(在 Java 中即延遲載入的概念)這個特性。
懶漢模式
public class UniqueSingleton {
//類載入時並未初始化
private static uniqueSingleton instance;
private UniqueSingleton() {
System.out.println(("UniqueSingleton is created"));
}
public static UniqueSingleton getInstance() {
if (instance == null) {
instance = new UniqueSingleton();
}
return instance;
}
}
複製程式碼
為了解決這個問題,我們自然得考慮到延遲載入,解決辦法也非常簡單,相信你也一目瞭然即在在建立之前加個判斷條件。但是問題真的就全部解決了麼,其實不然,在單執行緒環境下,這確實是一種比較完美的方案,但是在多執行緒情況下呢?試想多個例項同時被建立,我們能想到的解決辦法通常是在整個getInstance
方法前加個synchronize
關鍵詞,但是這同時也帶來了很大的效能開銷,這並不是我們希望的。這裡不得不提到網上一個大神的問答,他提出一個解決辦法——use an enum。
列舉類實現
public enum EnumExample {
INSTANCE;
private final String[] favoriteComic =
{ "fate", "Dragon Ball" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteComic));
}
}
複製程式碼
以上方法除了很簡單以外,enum
還提供了序列化機制,防止重新建立新的物件,這個回答也在 Stack Overflow 上獲得了最高票的回答。
object 實現
object 關鍵字建立一個新的單例型別,就像一個 class 只有一個被命名的例項。如果你熟悉 Java ,在 Scala 中宣告一個 object 有些像建立了一個匿名類的例項。 ——引自 Scala 函數語言程式設計
其實我在上面羅列了這麼多 Java 對於單例模式的實現方法以及對於不同場景所進行的不斷的優化,就是為了引出object
,因為object
就不必考慮這麼多,不會像 Java 那樣受到場景的約束。
舉個例子:
object Singleton {
def sum(l: List[Int]): Int = l.sum
}
複製程式碼
看起來程式碼是不是瞬間優雅了許多。如果感興趣的話,你可以利用$ javap
反編譯一下,可以看到所有方法前都帶上了static
關鍵詞,在這裡我就不在詳述。
因為執行緒安全再也不用擔心是單執行緒還是多執行緒,又或是考慮到延遲載入也只要加上lazy
關鍵詞按需初始化即可,方不方便誰用誰知道。
優化傳統工廠模式
眾所周知,在敲程式碼中我們做的最多的事情之一就是先去建立一個物件,這跟我們建房子先建地基一個道理。我們希望有一種模式能讓我們更好去使用它,方便後期的維護,建立型模式也就應運而生,而工廠模式又是建立型模式中的主角之一,我想這設計模式對熟悉 Java 的各位來說應該是小菜一碟吧。實際上,工廠模式被分成了三類——簡單工廠模式、工廠模式以及抽象工廠模式。但我更希望把它分為兩類,在我看來,簡單工廠模式更像是工廠模式的一個特例,不能算是嚴格意義上的模式,可它確確實實實現了建立例項的邏輯與客戶端的解耦。
在這裡,我會通過 Scala 和 Java 兩種不同的語言來實現簡單工廠模式,從而加深你對object
的印象。假設現在有個電腦器材製造廠,同時生產滑鼠和鍵盤,我們用熟悉的簡單工廠模式設計來描述它的業務邏輯。先用 Java 來定義:
//定義產品介面
public interface Product{
public void show();
}
//以下實現了具體產品類
public class Mouse implements Product {
@Override
public void show() {
System.out.println("A mouse has been built");
}
}
public class Keyboard implements Product {
@Override
public void show(){
System.out.println("A keyboard has been built");
}
}
public class SimpleFactory {
public Product produce(String name) {
switch (name) {
case "Mouse":
return new Mouse();
case "Keyboard":
return new Keyboard();
default:
throw new IllegalArgumentException();
}
}
}
//簡單使用
public class Test {
public static void main(String[] args) {
SimpleFactory simpleFactory = new SimpleFactory();
Mouse mouse = simpleFactory.produce("Mouse");
mouse.show();
}
}
複製程式碼
上述程式碼通過呼叫SimpleFactory
中的 produce
方法來建立不同的 Product 子類物件,從而實現建立例項的邏輯與客戶端之間解耦,在這裡我採用直接判斷傳入的 key 的方式來實現了簡單工廠模式,當然還有其他方式——通過 newInstance 反射等等。那麼有人會問,通過 Scala 該怎麼實現呢?這時候就要請我們的主角——單例物件出場了。
用單例代替工廠類
要知道 Scala 支援用object
來實現Java中的單例模式,所以我們可以實現一個SimpleFactory
單例,而不是一個工廠類,具體程式碼如下:
trait Product {
def show()
}
case class Mouse() extends Product {
def show = println("A mouse has been built")
}
case class Keyboard() extends Product {
def show = println("A keyboard has been built")
}
object SimpleFactory {//object代替class
def produce(name: String): Product = name match {
case "Mouse" => Mouse()
case "Keyboard" => Keyboard()
}
}
object Test extends App {
val mouse: Mouse = SimpleFactory.produce("Mouse")
mouse.show()
}
複製程式碼
通過以上程式碼,我們可以發現,同樣是通過判斷傳值的方式,Scala 也可以輕而易舉地實現。但這並不是最重要的,值得讓我們注意到的是在測試之前不用再去建立SimpleFactory
物件了,這正是先前講的object
靜態屬性在應用層次給我們帶來的便利,或許你會嗤笑這小小簡化才有多大的好處。別急,Scala 還為我們提供了一種語法糖 —— apply
,它本質上類似一個構造方法,這在上篇文章中也有講到,其實它也可以應用於工廠模式,通過這種方式,我們可以省略工廠類,只需增加產品類介面的伴生物件就可以實現。
伴生物件建立靜態工廠方法
我們通過判斷傳入的 name 字串來建立不同的物件,所以這裡的produce()
方法就顯得有點冗餘,如何讓工廠模式的實現更加的完美呢?用伴生物件來建立恰好可以解決這個問題:
object Product {
def apply(name: String): Product = name match {
case "Mouse" => Mouse()
case "Keyboard" => Keyboard()
}
}
複製程式碼
然後,我們就可以如此呼叫
val mouse: Product = Product("Mouse")
val keyboard: Product = Product("Keyboard")
mouse.show()
keyboard.show()
複製程式碼
這樣以後,呼叫的體驗是不是更好了呢?可以看到,利用object
的特性,我們在一定程度上改進了 Java 中設計模式的實現,簡單工廠模式僅僅也是冰山一角而已。
由於篇幅有限再次只列出簡單工廠模式,至於方法工廠模式和抽象工廠模式,有興趣的話可以看看原始碼。
最後,總結成一句話,object
作為一種打破靜態迴歸常態、天然的語言特性,它對 Java 的部分特性進行了優化以便我們能跟好地去理解、去使用,不知道你有沒有對此和我產生一些共鳴呢?