Factory Method:工廠化的物件生產方法

Factory Method UML 本篇文章將介紹在軟體設計模式 (Design Patterns) 中相當著名而且受到廣泛使用的 Factory Method 模式,以及 Factory Method 在遊戲程式設計難題中的實際應用。

想要熟悉 Factory Method 設計模式,首先要瞭解下列幾項問題:

  • 這個設計模式的目的是什麼?
  • 這個設計模式能夠用來解決什麼樣的問題?
  • 這個設計模式要如何應用在遊戲程式之中?

或許對於程式設計者來說,十個人之中有九個半討厭聽到「工廠」或「工廠化管理」的相關術語,但是在軟體設計模式的領域中,工廠化的生產作業方式反而是相當實際而且管用的方法。Factory Method 是軟體領域中,非常基礎同時也非常重要的設計模式之一,或許你從來沒有在書本中學習過相關的知識,但是已經在撰寫程式的經驗過程中,處處使用著這樣的設計方法而不自知。如果能夠進一步瞭解這些前人思維架構的心血結晶,將有助於改善現有的程式架構,並且提升未來撰寫程式系統架構的能力。

在 Factory Method 中的「工廠」,意指製造具體「成品」的場所,也就是在程式中創建出具體物件的創造者。所以 Factory Method 在設計模式的分類中屬於「生成模式」的一種,目的是將物件的具現化過程加以抽象化而提取出來,用來處理與物件相關的創建行為與毀滅行為。根據《物件導向設計模式》(Design Patterns) 書中 Factory Method 的 UML 結構圖描述,定義了參與工廠方法模式的四個角色:

  • Product:定義 Factory Method 所造物件的介面。
  • ConcreteProduct:具體實作出 Product 介面。
  • Creator:宣告 Factory Method,它會傳回 Product 型別之物件。
  • ConcreteCreator:覆寫 Factory Method 以傳回 ConcreteProduct 的物件個體。

這裡的 Creator 扮演著抽象化工廠的角色,僅宣告出可供衍生類別覆寫的成員函式;而真正要製作出實體成品的實做細節,則是交由繼承自 Creator 的實作工廠 ConcreteCreator 所定義。如 UML 結構圖所示,在 ConcreteCreator 中,就可以指涉並且生成真正的實體成品 ConcreteProduct。就像是鞋子工廠只能製造鞋子,帽子工廠只能製造帽子,在工廠方法模式中,通常是一個「創造者」搭配一項「產品」,所以「實體工廠」與「實體產品」的類別定義,經常會以成對的模式出現在程式系統之中。

以 3D 程式設計中的創建貼圖 (Create Texture) 程序為例,由於貼圖生成動作是與繪圖渲染器 (Renderer) 高度相依的行為,所以在一般的情況之下,程式設計者多半會選擇將貼圖相關的程序包含在 Renderer 類別的定義中;假設我們使用 Direct3D 的繪圖 API 進行從檔案創建貼圖的程序,程式碼應該會像是:

// Direct3D version
void GraphicsRenderer::CreateTexture(std::string& _rkTextureName)
{
    D3DXCreateTextureFromFile(m_pD3DDevice, _rkTextureName.c_str(), m_ppkTexture);
}

GraphicsRenderer 是繪圖渲染器的處理類別,在 CreateTexture() 函式中,直接使用 D3DX Library 的輔助函式 D3DXCreateTextureFromFile() 就能夠順利地讀取檔案並且建立貼圖。這是看起來沒有任何問題的程式碼。

然而,如果在某些不可抗力的情況之下,需要將原本使用的繪圖 API 由 Direct3D 轉換至 OpenGL 時,該怎麼做?

不管是因為進行全新的第二個專案,還是因為換了 OpenGL 派的程式主管,甚至是因為微軟突然間被惡性併購倒閉。要換成 OpenGL 的繪圖程序?很簡單,只要把相關的貼圖處理程式碼置換掉就沒問題了:

// OpenGL version
void GraphicsRenderer::CreateTexture(std::string& _rkTextureName)
{
    m_pvImageData = LoadImage(_rkTextureName);
    glGenTextures(1, &m_iTextureID);
}

OK,同樣沒有任何問題。由於你的傑出表現,前面的兩個遊戲專案都相當地成功而且賣座,到了第三個專案,終於有足夠的經費可以購買 3D 引擎來開發遊戲了!

要如何能夠把原有的程式碼,轉換成適用外部 3D 引擎的用法呢?同樣地,把之前 OpenGL 相關的部分刪除,新增外部 3D 引擎的 API 即可,函式介面完全沒變:

// SuperEngine version
void GraphicsRenderer::CreateTexture(std::string& _rkTextureName)
{
    SuperEngine::LoadImage(_rkTextureName);
    SuperEngine::CreateTexture(_rkTextureName);
}

看起來一切正常。然而,在上述的情境中,雖然保全了函式介面的不變性與重複利用性 (Reusablility),但每次更換繪圖 API 時,程式設計者卻不得不在新舊的程式碼的函式實作版本間,使用 Ctrl-C 與 Ctrl-V 進行大量的複製、貼上、刪除與修改的動作。以上述的程式碼範例來看,複製貼上且進行必要的修改並不是什麼困難的事情,但是當專案的規模成長到某種程度,例如數百個程式碼檔案、數萬行程式碼的時候,往往會令程式設計者望而生畏。與其大興土木地從舊有的程式碼中挖牆修補,許多程式設計者最後會選擇將程式架構整個打掉重建,而很遺憾地無法達到程式碼的重複利用性

在此如果運用 Factory Method 設計模式,就能夠妥善並且優雅地解決這個貼圖物件生成模式的難題。根據《物件導向設計模式》所述,Factory Method 的使用時機有下列三項:

  • 當類別無法明指欲生成的物件類別時。
  • 當類別希望讓子類別去指定欲生成的物件類型時。
  • 當類別將權力下放給一個或多個輔助用途的子類別,你又希望將「交付給哪些子類別」的知識集中在一處時。

在這個例子中,我們需要產生出不同繪圖 API 實作版本的工廠類別,以及與工廠相對應的實體產品類別,因此相當符合第二項的使用時機。在新的程式架構中,首先需要將創建貼圖的程序導向化設計,改變成物件導向化的設計,也就是將所有與貼圖相關的屬性和操作封裝成為一個 Texture 類別。在此 GraphicsRenderer 所扮演的是 Factory Method 中的創造者角色,而 Texture 就是由 GraphicsRenderer 所創建出來的產品

// @file Texture.h
class Texture
{
public:
    Texture();
    virtual ~Texture();
    
    virtual void Load(std::string& _rkFileName);
    std::string GetFileName() const;
    
protected:
    std::string m_kFileName;
};

基本上,由於 Texture 扮演的是抽象化的角色,所以類別的內容非常簡單,除了記錄檔案名稱的屬性之外,最重要的是宣告用來讀取檔案的虛擬成員函式 Load()。

接著是 GraphicsRenderer 類別,對於創建貼圖的操作也非常簡單,只要接受一個字串參數,然後回傳創建完成的 Texture 物件指標即可:

// @file GraphicsRenderer.h
class Texture;

class GraphicsRenderer
{
public:
    GraphicsRenderer();
    virtual ~GraphicsRenderer();

    virtual Texture* CreateTexture(std::string& _rkTextureName);
}

// @file GraphicsRenderer.cpp
#include "Texture.h"
Texture* GraphicsRenderer::CreateTexture(std::string& _rkTextureName)
{
    return NULL;
}

按照 Factory Method 的 UML 結構圖所示,有了抽象化的 Product 類別與 Creator 類別之後,就可以開始依實作需求創造出不同版本的 ConcreteProduct 類別與 ConcreteCreator 了。以 Direct3D 的實作版本為例,把與 Direct3D 相關的結構都存放在這個新建的 D3DTexture 類別中:

// @file D3DTexture.h
class D3DTexture : public Texture
{
public:
    D3DTexture();
    virtual ~D3DTexture();
    
    virtual void Load(std::string& _rkFileName);
    
private:
    LPDIRECT3DTEXTURE9* m_ppkTexture;
};

// @file D3DTexture.cpp
void D3DTexture::Load(std::string& _rkFileName)
{
    D3DXCreateTextureFromFile(m_pD3DDevice, _rkFileName.c_str(), m_ppkTexture);
}

Direct3D 版本的渲染器類別 D3DGraphicsRenderer 繼承自抽象化的 GraphicsRenderer 類別,定義如下:

// @file D3DGraphicsRenderer.h
class D3DGraphicsRenderer : public GraphicsRenderer
{
public:
    D3DGraphicsRenderer ();
    virtual ~D3DGraphicsRenderer ();

    virtual Texture* CreateTexture(std::string& _rkFileName);    
}

// @file D3DGraphicsRenderer.cpp
#include "D3DTexture.h"

Texture* D3DGraphicsRenderer::CreateTexture(std::string& _rkFileName)
{
    return new D3DTexture(_rkFileName);
}

有了抽象化的 Texture 類別與 GraphicsRenderer 類別,再衍生出 D3DTexture 類別與 D3DGraphicsRenderer 類別之後,就完成了一組 Factory Method 的類別組成架構。可以開始輕鬆簡單地生產 Direct3D 版本的 Texture 物件了:

// @file FactoryMethod.cpp
void main()
{
    // Create D3D renderer
    GraphicsRenderer* pkD3DRenderer= new D3DGraphicsRenderer();

    // Create textures
    std::string kFileName1 = "123.jpg";
    Texture* pkTex1 = pkD3DRenderer->CreateTexture(kFileName1);
    std::string kFileName2 = "abc.png";
    Texture* pkTex2 = pkD3DRenderer->CreateTexture(kFileName2);
    std::string kFileName3 = "tree.bmp";
    Texture* pkTex3 = pkD3DRenderer->CreateTexture(kFileName3);

    // Destroy textures
    pkD3DRenderer->DestroyTexture(pkTex3);
    pkD3DRenderer->DestroyTexture(pkTex2);
    pkD3DRenderer->DestroyTexture(pkTex1);

    // Destroy D3D renderer
    delete pkD3DRenderer;
}

如果要增加 OpenGL 版本的創建貼圖程序,只要仿照上述的方式,再定義出一組新的 OGLTexture 類別與 OGLGraphicsRenderer 類別,就能夠輕易地達成更換實作版本的目標。除了一開始對於實體工廠的選擇,需要明確指定出要使用的是 D3DGraphicsRenderer 工廠或 OGLGraphicsRenderer 工廠之外,往後就能夠完全忽略實作層面的細節,僅需要對抽象化工廠 GraphicsRenderer 所提供的介面進行與貼圖相關的操作行為。

有了 Factory Method 設計模式,就不必再將與應用場合高度相依的類別寫死在主程式裡。主程式只須面對 Product 介面,因此可和任何未知的 ConcreteProduct 類別合作無間。

而在上述的例子中,有了創建貼圖的設計模式後,很自然地能夠再增加如 DestroyTexture() 的成員函式處理貼圖毀滅的程序。怎麼來的就怎麼回去;由工廠所創造出來的產品,就交由工廠進行毀滅的程序。所以在新增了 DestroyTexture() 成員函式之後,就能夠讓使用者傳入 Texture 物件指標或是 Texture 的名稱,於實體工廠中對貼圖資源進行毀滅或暫時回收的動作。而同樣地,使用者無須去操心煩惱相關的處理細節,只要全部丟給工廠處理就沒問題了!另外,當然也可以在工廠類別內部,使用改良版的 STL 容器存放這些貼圖物件,以達到集中管理的功用。

不論工廠內部的管理方法、製造流程如何地更換變動,都不會影響到系統外部使用者的操作行為,這就是使用 Factory Method 模式所帶來的程式架構益處。對於系統外部的使用者來說,以 Factory Method 實現出來的程式組件,就像是真實的工廠般準確無誤的運作:只要將事先約定好的資訊(貼圖檔案名稱)交遞給工廠,它就能夠生產出使用者所要求的產品(貼圖物件)。使用者通常並不會也不需要關心成品的生產過程或生成模式,因此利用 Factory Method 創建物件產品,正好能夠完善地封裝隱藏物件生成過程的實做細節。如同黑盒子的工作程序一樣,使用者不需要去瞭解產品究竟是使用 new operator 配置記憶體,或是利用資源回收系統產生出要求的物件,只管直接使用創建出來的物件即可。

Factory Method Applied Example 左圖就是上述創建貼圖範例的 UML 結構圖示,不妨與文章開頭的 Factory Method UML 結構圖兩相比對。是不是能夠對於這個有趣又實用的設計模式有了基礎的認識?

在這裡瞭解了 Factory Method 設計模式的實現目的,以及運用在遊戲程式難題中的實用性之後,其實只完成了對於工廠設計模式的一半認識。

工廠設計模式的另外半部知識,也就是 Factory Method 的好兄弟——Abstract Factory 模式,即將在續篇中接力上場,欲知結局如何,靜待續集分曉!

程式碼範例下載:FactoryMethod_SampleCode.zip (下載次數: 1816 )

2 Replies to “Factory Method:工廠化的物件生產方法”

  1. 您好~
    您的文章寫的都非常棒耶~
    我也在遊戲業工作
    看了您的文章後覺得很有收穫
    希望您能繼續寫下去歐~:D

    半路:
    你好,
    非常感謝您的支持,
    如果對於文章內容有任何想法,還請不吝指教~ :D

  2. 我认为有些找不到合适的factory class的类在内部public部分加入一个 static的方法返回instance指针也很好,就像singleton,但是每次都new,然后初始化,return,可以达到同样效果,而且免去专门创建factory class

Leave a Reply