1. 程式人生 > >學習設計模式之一:Factory Method

學習設計模式之一:Factory Method

基本概念

FactoryMethod是一種建立性模式,它定義了一個建立物件的介面,但是卻讓子類來決定具體例項化哪一個類.當一個類無法預料要建立哪種類的物件或是一個類需要由子類來指定建立的物件時我們就需要用到Factory Method 模式了.簡單說來,Factory Method可以根據不同的條件產生不同的例項,當然這些不同的例項通常是屬於相同的型別,具有共同的父類.Factory Method把建立這些例項的具體過程封裝起來了,簡化了客戶端的應用,也改善了程式的擴充套件性,使得將來可以做最小的改動就可以加入新的待建立的類. 通常我們將Factory Method作為一種標準的建立物件的方法,當發現需要更多的靈活性的時候,就開始考慮向其它建立型模式轉化

簡單分析

圖1是Factory Method 模式的結構圖,這裡提供了一些術語,讓我們可以進行更方便的描述:

  1. Product: 需要建立的產品的抽象類.
  2. ConcreteProduct: Product的子類,一系列具體的產品.
  3. Creator: 抽象建立器介面,宣告返回Product型別物件的Factory Method.
  4. ConcreteCreator: 具體的建立器,重寫Creator中的Factory Method,返回ConcreteProduct型別的例項

圖1: Factory Method 模式結構
圖1: Factory Method 模式結構

由此可以清楚的看出這樣的平行對應關係: Product <====> Creator ; ConreteProduct <====> ConreteCreator

抽象產品對應抽象建立器,具體產品對應具體建立器.這樣做的好處是什麼呢?為什麼我們不直接用具體的產品和具體的建立器完成需求呢?實際上我們也可以這樣做.但通過Factory Method模式來完成,客戶(client)只需引用抽象的Product和Creater,對具體的ConcreteProduct和ConcreteCreator可以毫不關心,這樣做我們可以獲得額外的好處:

  • 首先客戶端可以統一從抽象建立器獲取產生的例項,Creator的作用將client和產品建立過程分離開來,客戶不用操心返回的是那一個具體的產品,也不用關心這些產品是如何建立的.同時,ConcreteProduct也被隱藏在Product後面,ConreteProduct繼承了Product的所有屬性,並實現了Product中定義的抽象方法,按照Java中的物件造型(cast)原則,通過ConcreteCreator產生的ConcreteProduct可以自動的上溯造型成Product.這樣一來,實質內容不同的ConcreteProduct就可以在形式上統一為Product,通過Creator提供給client來訪問.
  • 其次,當我們新增一個新的ConcreteCreator時,由於Creator所提供的介面不變,客戶端程式不會有絲毫的改動,不會帶來動一發而牽全身的災難, 這就是良好封裝性的體現.但如果直接用ConcreteProduct和ConcreteCreator兩個類是無論如何也做不到這點的. 優良的面向物件設計鼓勵使用封裝(encapsulation)和委託(delegation),而Factory Method模式就是使用了封裝和委託的典型例子,這裡封裝是通過抽象建立器Creator來體現的,而委託則是通過抽象建立器把建立物件的責任完全交給具體建立器ConcreteCreator來體現的.

現在,請再回頭看看基本概念中的那段話,開始也許覺得生澀難懂,現在是不是已經明朗化了很多.

下面讓我們看看在 Java 中如何實現Factory Method模式,進一步加深對它的認識.

具體實施

先說明一點,用Factory Method模式建立物件並不一定會讓我們的程式碼更短,實事上往往更長,我們也使用了更多的類,真正的目的在於這樣可以靈活的,有彈性的建立不確定的物件.而且,程式碼的可重用性提高了,客戶端的應用簡化了,客戶程式的程式碼會大大減少,變的更具可讀性.

  1. 標準實現: 這裡我採用Bruce Eckel 用來描述OO思想的經典例子 Shape.這樣大家會比較熟悉一些.我完全按照圖1中所定義的結構寫了下面的一段演示程式碼.這段程式碼的作用是建立不同的Shape例項,每個例項完成兩個操作:draw和erase.具體的建立過程委託ShapeFactory來完成.

    1.a 首先定義一個抽象類Shape,定義兩個抽象的方法.

    abstract class Shape {
      // 勾畫shape
      public abstract void draw();
      // 擦去 shape
      public abstract void erase();
      public String name;
      public Shape(String aName){
        name = aName;
      }
    }
    1.b 定義 Shape的兩個子類: Circle, Square,實現Shape中定義的抽象方法
// 圓形子類
class Circle extends Shape {
  public void draw() {
    System.out.println("It will draw a circle.");
  }
  public void erase() {
    System.out.println("It will erase a circle."); 
  }
  // 建構函式
  public Circle(String aName){
    super(aName);
  }
}
// 方形子類
class Square extends Shape {
  public void draw() { 
    System.out.println("It will draw a square."); 
  }
  public void erase() { 
    System.out.println("It will erase a square."); 
  }
  // 建構函式
  public Square(String aName){
    super(aName);
  }
}
1.c 定義抽象的建立器,anOperation呼叫factoryMethod建立一個物件,並對該物件進行一系列操作.
abstract class ShapeFactory {  
  protected abstract Shape factoryMethod(String aName);
  // 在anOperation中定義Shape的一系列行為
public void anOperation(String aName){
    Shape s = factoryMethod(aName);
    System.out.println("The current shape is: " + s.name);
    s.draw();
    s.erase();
  }
}
1.d 定義與circle和square相對應的兩個具體建立器CircleFactory,SquareFactory,實現父類的methodFactory方法
// 定義返回 circle 例項的 CircleFactory
class CircleFactory extends ShapeFactory {
  // 過載factoryMethod方法,返回Circle物件
  protected Shape factoryMethod(String aName) {
    return new Circle(aName + " (created by CircleFactory)");
  }
}
  
// 定義返回 Square 例項的 SquareFactory
class SquareFactory extends ShapeFactory {
  // 過載factoryMethod方法,返回Square物件
protected Shape factoryMethod(String aName) {
    return new Square(aName + " (created by SquareFactory)");
  }
}
1.e 測試類:請注意這個客戶端程式多麼簡潔,既沒有羅嗦的條件判斷語句,也無需關心ConcreteProduct和ConcreteCreator的細節(因為這裡我用anOperation封裝了Product裡的兩個方法,所以連Product的影子也沒看見,當然把Product裡方法的具體呼叫放到客戶程式中也是不錯的).
class Main {
  public static void main(String[] args){
    ShapeFactory sf1 = new SquareFactory(); 
    ShapeFactory sf2 = new CircleFactory();
    sf1.anOperation("Shape one");
    sf2.anOperation("Shape two");
  }
}  

執行結果如下:

The current shape is: Shape one (created by SquareFactory)

It will draw a square.

It will erase a square.

The current shape is: Shape two (created by CircleFactory)

It will draw a circle.

It will erase a circle.

2.引數化的Factory Method: 這種方式依靠指定的引數作為標誌來建立對應的例項,這是很常見的一種辦法.比如JFC中的BorderFactory就是個很不錯的例子. 以下的這個例子是用字串作為標記來進行判斷的,如果引數的型別也不一樣,那就可以用到過載函式來解決這個問題,定義一系列引數和方法體不同的同名函式,這裡java.util.Calendar.getInstance()又是個極好的例子.引數化的建立方式克服了Factory Method模式一個最顯著的缺陷,就是當具體產品比較多時,我們不得不也建立一系列與之對應的具體構造器. 但是在客戶端我們必須指定引數來決定要建立哪一個類.

2.a 我們在第一種方法的基礎上進行修改,首先自定義一個的異常,這樣當傳入不正確的引數時可以得到更明顯的報錯資訊.

class NoThisShape extends Exception {
  public NoThisShape(String aName) {
    super(aName);
  }
}

2.b去掉了ShapeFactory的兩個子類,改為由ShapeFactory直接負責例項的建立. ShapeFactory自己變成一個具體的建立器,直接用引數化的方法實現factoryMethod返回多種物件.
abstract class ShapeFactory {  
  private static Shape s;
  private ShapeFactory() {}
    
  static Shape factoryMethod(String aName, String aType) throws NoThisShape{
    if (aType.compareTo("square")==0)
      return new Square(aName);
    else if (aType.compareTo("circle")==0)
      return new Circle(aName);
    else throw new NoThisShape(aType);  
  }
  
  // 在anOperation中定義Shape的一系列行為
  static void anOperation(String aName, String aType) throws NoThisShape{
    s = factoryMethod(aName, aType);
    System.out.println("The current shape is: " + s.name);
    s.draw();
    s.erase();
  }
}

2.c 測試類:這裡客戶端必須指定引數來決定具體建立哪個類.這個例子裡的anOperation是靜態函式,可以直接引用.

class Main {
  public static void main(String[] args) throws NoThisShape{
    ShapeFactory.anOperation("Shape one","circle");
    ShapeFactory.anOperation("Shape two","square");
    ShapeFactory.anOperation("Shape three", "delta");
  }
}

執行結果如下:
The current shape is: Shape one
It will draw a circle.
It will erase a circle.
The current shape is: Shape two
It will draw a square.
It will erase a square.
Exception in thread "main" NoThisShape: delta
        at ShapeFactory.factoryMethod(ShapeFactory.java:10)
        at ShapeFactory.anOperation(ShapeFactory.java:15)
        at Main.main(Main.java:5)

3.動態裝載機制:

有的時候我們會把ConcreteProduct的例項傳給建立器作為引數,這種情況下,如果在建立器裡完成建立過程,就必須判斷引數的具體型別(用instanceof),然後才能產生相應的例項,那麼比較好的做法是利用Java的動態裝載機制來完成這件事.比如:

我們得到一個Shape的子類s,但不知道具體是那個子類,就可以利用Class類自帶的方法newInstance()得到例項

return (Shape)s.getClass().newInstance();

這種方法有興趣得讀者可以自己嘗試,限於篇幅,不寫具體程式碼出來了.

後話

看完這篇文章後,相信讀者對Factory Method模式有一個比較清楚的瞭解了.我想說的是,我們不僅應該關心一個具體的模式有什麼作用,如何去實現這個模式,更應該透過現象看本質,不但知其然,還要知其所以然.要通過對模式的學習加深對面向物件思想的理解,讓自己的認識得到昇華.Factory Method模式看似簡單,實則深刻.抽象,封裝,繼承,委託,多型,針對介面程式設計等面向物件中的概念都在這裡得到了一一的體現.只有抓住了它的本質,我們才能夠不拘於形式的靈活運用,而不是為了使用模式而使用模式.

參考資料