Scala 與設計模式(四):Factory 工廠模式

ScalaCool發表於2017-08-22

在中國歷史上,房子常常與當下一樣稀缺,住房問題同樣是一個讓百姓苦惱的社會熱點。

在拆違章建築還不盛行的年代,我們可以選擇在深山老林裡自己修建住所。在 Java 中可能是這樣實現的:

class BuildingA {
    private String name;

    public BuildingA(String name) {
        this.name = name;
    }

    public void build() {
        System.out.println(name + " is building");
    }
}

class BuildingB {
    private String name;

    public BuildingB(String name) {
        this.name = name;
    }

    public void build() {
        System.out.println(name + " is building");
    }
}

// 使用
  BuildingA buildingA = new BuildingA("bedroom");
  BuildingB buildingB = new BuildingB("kitchen");
  buildingA.build();
  buildingB.build();複製程式碼

村裡的牛大哥在建完兩間房子之後,後知後覺:自己想要的房間格局不同,但是風格得相同,可以把公共的部分抽離出來:

interface IBuilding {
    void build();
}

abstract class AbstractBuilding implements IBuilding {
    protected void buildCommon(){
        System.out.println("Europe style"); // 公共的部分
    }
}

 class BuildingAs extends AbstractBuilding {
    private String name;
    public BuildingAs(String name){
        this.name = name;
    }
    @Override
    public void build() {
        this.buildCommon();
        System.out.println(name + " is building");
    }
}

 class BuildingBs extends AbstractBuilding {
    private String name;
    public BuildingBs(String name) {
        this.name = name;
    }

    @Override
    public void build() {
        this.buildCommon();
        System.out.println(name + " is building");
    }
}

// 使用
BuildingAs buildingA = new BuildingAs("bedroom");
BuildingBs buildingB = new BuildingBs("kitchen");
buildingA.build();
buildingB.build();複製程式碼

但是這麼做之後,牛大哥發現在建造的時候並沒有省力,他向村口的王師傅請教,為什麼我考慮了很多反而沒什麼作用呢?

簡單工廠模式

王師傅告訴他:雖然你找出了一些公共的流程,但在實際建造過程中,你還是完整的過了所有的流程(構造方法不同,每次都要 new 物件)。另外,

另外,你對房屋的需求並不多,所以優勢不夠明顯。

說著掏出一個寶盒,盒子裡有很多設計圖:下次你可以委託我來造一些元件(不再需要自己 new):

public class SimpleFactory {
    public static IBuilding getProduct(String name){
        if("bedroom".equals(name)){
            return new BuildingA(name);
        }else if("kitchen".equals(name)){
            return new BuildingB(name);
        }else{
            throw new IllegalArgumentException();
        }
    }
}

// 使用
IBuilding buildingA =  SimpleFactory.getProduct("bedroom");
IBuilding buildingB =  SimpleFactory.getProduct("kitchen");
buildingA.build();
buildingB.build();複製程式碼

王師傅幫助下的牛大哥在後面的建造中感覺輕鬆多了。

這就是「簡單工廠模式」,也稱作「靜態工廠方法模式」。

優點

它有以下幾個優點:

  • 簡化物件建立的 API
  • 減少 new 關鍵字對程式碼的干擾
  • 程式碼更精簡優雅

而牛二哥明顯沒有那麼幸運,他的妻子追求個性,並且很善變,總是在建造過程中更改需求。

缺點

雖然牛二哥也去王師傅那獲取元件,每次王師傅都要拿出他的寶盒,在裡面翻一遍,再告訴牛二哥 —— 這個我不會造。站在 OCP(開放封閉原則)的角度講,該模式的擴充套件不夠良好,每次有新的模型後都要修改工廠。

工廠方法模式

老王師傅也經不起折騰,想著不能閉關鎖國,就把自己會建造的元件貼在顯眼的地方,有新的元件直接加在上面就好:

interface IFactory {
    public IBuilding createBuilding();
}

class FactoryA implements IFactory{

    @Override
    public IBuilding createBuilding() {
      // 可以進行復雜的處理,每一種方法對應一種模型
        return new BuildingA("bedroom");
    }
}

class FactoryB implements IFactory{

    @Override
    public IBuilding createBuilding() {
        return new BuildingA("kitchen");
    }
}

class FactoryC implements IFactory{

    @Override
    public IBuilding createBuilding() {
        return new BuildingA("restroom");
    }
}

// 使用
FactoryA factoryA = new FactoryA();
FactoryB factoryB = new FactoryB();
FactoryC factoryC = new FactoryC();
factoryA.createBuilding();
factoryB.createBuilding();
factoryC.createBuilding();複製程式碼

這樣大家的溝通是方便了很多,而且老王也不用每次都搜一遍傳家寶盒。

這種模式被 GOF 稱作「工廠方法模式」。

定義

工廠方法模式(Factory Method Pattern)是一種實現了「工廠」概念的物件導向設計模式。就像其他建立型模式一樣,它也是 處理在不指定物件具體型別的情況 下建立物件的問題。定義如下:

定義一個用於建立物件的介面,讓子類決定例項化哪一個類。Factory Method 使一個類的例項化延遲到其子類。 — 《設計模式》 GOF

從以上也可看出:工廠做的事很簡單 —— 封裝內部的實現細節

優點

它可以帶來以下好處:

  1. 降低耦合度。在工廠方法模式中,工廠方法用來建立使用者所需要的產品,同時還向使用者隱藏了哪種具體產品類將被例項化這一細節,使用者只需要關心所需產品對應的工廠,無須關心建立細節,甚至無須知道具體產品類的類名。
  2. 良好擴充套件性。需要新的產品型別時,只需要新增一個工廠類,不需要更改產品以及產品的介面以及使用者的使用方式。
  3. 程式碼結構清晰。使用者使用時不需要構造內部結構,直接呼叫工廠方法即可。

缺點

我們可能會遇上以下問題:

  1. 增加使用成本。使用工廠模式的時候一般都會採用介面或者抽象類,有時還會涉及到反射、DOM 等方式。
  2. 增加系統複雜度(影響不顯著)。一個工廠類對應一個產品,所以增加產品類的時候就需要增加工廠類。

工廠方法模式針對的是一個產品等級結構,當要處理多個產品等級結構時(ex. 建立不同小區,小區裡有不同樓宇,樓裡還有不同戶型),我們不希望對每個模型都建立一個工廠,這太糟糕了,來看看「抽象工廠模式」是如何解決的。

抽象工廠模式

定義

為建立一組相關或相互依賴的物件提供一個介面,而且無需指定他們的具體類。

我們也可把「一組相關或相互依賴的物件」稱作「產品族」。

利用抽象工廠,我們可以這麼寫:

interface IBuildingA {
    void buildA();
}

interface IBuildingB {
    void buildB();
}

interface IFactory {
    public IBuildingA createBuildingA();
    public IBuildingB createBuildingB();
}

class BuildingA implements IBuildingA {
    ... // 省略建構函式
    @Override
    public void buildA() {
        System.out.println((name + "is building"));
    }
}

class BuildingB implements IBuildingB {
    ... // 省略建構函式
    @Override
    public void buildB() {
        System.out.println(name + " is building");
    }
}

class Factory implements IFactory{
    @Override
    public IBuildingA createBuildingA() {
        return new BuildingA("big bedroom");
    }

    @Override
    public IBuildingB createBuildingB() {
        return  new BuildingB("small bedroom");
    }
}

// 測試
Factory factory = new Factory();
factory.createBuildingA();
factory.createBuildingB();複製程式碼

我們可以直接在一個工廠類中實現多個方法,這樣不用管理多個工廠,使用和管理起來都更方便。

如果說工廠方法解決問題的方式是「廣搜」,那抽象工廠亦可看作「深搜」。

總結

以上,我們使用到了三種設計模式:簡單工廠(靜態工廠方法)工廠方法抽象工廠
在三種模式中,我們要做的都是將工廠的初始化與構造分離

雖然比起直接 new 要增加不少程式碼,但在後期維護的時候,能給我們提供很多的便利。

看完 Java 版本,我們再來看看 Scala 是如何實現的。

Scala 實現

在 Scala 中,依舊可以用類似 Java 的方式來實現,只用把 Java 中的關鍵字 interface 換成 trait 即可,直接看程式碼吧。

簡單工廠模式

trait IBuilding {
  def show()
}

case class SimpleBuilding(name: String)extends IBuilding {
  def show = println("SimpleBuilding " + name + " is building")
}

case class LuxuryBuilding(name: String) extends IBuilding{
  def show = println("LuxuryBuilding " + name + " is building")
}

object ConstructionFactory  {
  def createBuilding(kind: String): IBuilding =  kind match {
    case "Simple" =>   SimpleBuilding("Simple")
    case "Luxury" =>   LuxuryBuilding("Luxury")
  }
}

object Test extends App {
  val simpleBuilding: IBuilding = ConstructionFactory.createBuilding("Simple")
  val luxuryBuilding: IBuilding = ConstructionFactory.createBuilding("Luxury")
  simpleBuilding.show()
  luxuryBuilding.show()
}複製程式碼

除了這種方式,Scala 還為我們提供了一種類似構造器的語法 —— apply,通過這種方式,我們可以省略工廠類,只需增加產品類介面的伴生物件:

object IBuilding {
  def apply(kind: String): IBuilding = kind match {
    case "Simple" =>   SimpleBuilding("Simple")
    case "Luxury" =>   LuxuryBuilding("Luxury")
  }
}複製程式碼

呼叫者有更好的體驗:

val simpleBuilding: IBuilding = IBuilding("Simple")
val luxuryBuilding: IBuilding = IBuilding("Luxury")
simpleBuilding.show()
luxuryBuilding.show()複製程式碼

嚴格意義講,這種方法並不屬於 GOF 提到的工廠方法,它缺少了工廠模組,我們可以稱之為「靜態工廠模式」。

工廠方法與抽象工廠的實現與 Java 類似,程式碼就不貼出來了。不瞭解 Scala 的同學可以參考原始碼

總結

內部組成

以上,不難總結出工廠模式中的四種角色(簡單工廠模式中沒有抽象工廠):

  • 抽象產品:它是定義產品的介面,是工廠方法模式所建立物件的超型別,也就是產品物件的公共父類。(ex. 文中 IBuiiding)。
  • 具體產品:它實現了抽象產品介面,某種型別的具體產品由專門的具體工廠建立,具體工廠和具體產品之間一一對應。(ex. 文中 Buiiding)
  • 抽象工廠: 在抽象工廠類中,宣告瞭工廠方法(Factory Method),用於返回一個產品。抽象工廠是工廠方法模式的核心,所有建立物件的工廠類都必須實現該介面。(ex. 文中 IFactory)
  • 具體工廠: 它是抽象工廠類的子類,實現了抽象工廠中定義的工廠方法,並可由客戶端呼叫,返回一個具體產品類的例項。(ex. 文中 ConstructionFactory)

適用場景

當然,我們不能為了設計而設計,當類結構簡單的時候,我們可以直接使用 new 來創造,否則會增加不必要的程式碼,反而使結構複雜化。

所有工廠模式適用場景類似:呼叫者無需知道他所使用的物件的類(實際上內部結構對呼叫者是透明的 ex. 簡單工廠)。

但還是有所差異,以下為個人理解:

名稱 適用場景
簡單工廠 1. 工廠類負責建立的物件比較少
2. 客戶只知道傳入工廠類的引數,對於如何建立物件(邏輯)不關心
工廠方法 工廠類負責建立的物件複雜, 且內部物件層級關係比較簡單
抽象工廠 工廠類負責建立的物件複雜, 且內部物件層級關係比較複雜

原始碼連結

如有錯誤和講述不恰當的地方還請指出,不勝感激!

相關文章