從 Java 到 Scala (三): object 的應用

ScalaCool發表於2018-09-10

本文由 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 的部分特性進行了優化以便我們能跟好地去理解、去使用,不知道你有沒有對此和我產生一些共鳴呢?

相關文章