Tuesday, May 06, 2014

Object Factories, Ch8, Modern C++ Design

這章是從 Factory Pattern 出發的,也稍微解在什麼情形需要用 Factory Pattern。使用 Factory Pattern 的時候,常會出現固定的模式,只有 type 不一樣,但 code flow 很相似的情況,像是下面的例子。
class Fruit{};
class Apple : public Fruit{};
class Orange : public Fruit{};
class Grape : public Fruit{};

class FruitFactory
{
public:
    enum EN_FRUIT_TYPE
    {
        E_APPLE,
        E_ORANGE,
        E_GRAPE,
    };

    static FruitFactory* getInstance(){...}

    Fruit* create(EN_FRUIT_TYPE enType)
    {
        switch(enType)
        {
            case E_APPLE:
                return new Apple();
            case E_ORANGE:
                return new Orange();
            case E_GRAPE:
                return new Grape();
        }
    }
};
...
Fruit* foo = FruitFactory::getInstance()->create(FruitFactory::E_APPLE);

上面的例子裡,最明顯的特徵就是有個 create(),根據參數透過 switch 來決定要產生哪個實體。我個人每次寫這段的時候,都要檢查再三,確定 E_APPLE 真的是對應 new Apple(),因為每個 case 都很類似,所以複製貼上再修改,很怕眼花改漏改錯。我想,很多書上不建議用 switch 這是其中一個理由。

像這樣重複的 code flow 的確是 template 的好戲。這章的作法,是把 Factory 寫成 class template,並且把 switch-case 用一個 map 取代。如此一來,需要 Factory class 的時候,只要套用 template 就可。要注意,map 跟 switch 的本質不同,map 是個資料結構,是一個變數,要像switch 一樣到處都可以呼叫,就要是 global 的。也就是說,這裡假設 Factory class 一定是 singleton 或者 mono-state。像上面的 FruitFactory::create() 沒有使用 local 變數,很容易滿足這個條件,這也是最常見的情況。

template<
    class ProductType,
    typename IdType,
    typename Creator = Fruit* (*)()>
class Factory
{
public:
    static Factory* getInstance()
    {
        static Factory s_instance;
        return &s_instance;
    }

    bool registerType(IdType id, Creator creator)
    {
        m_map[id] = creator;
        return true;
    }
    ProductType* create(IdType id)
    {
        if(m_map.end() == m_map.find(id))
        {
            return NULL;
        }
        return m_map[id]();
    }

private:
    std::map<IdType, Creator> m_map;
};
...
typedef Factory<Fruit, int> FruitFactory2;

Factory template 必要的參數是 ProductType,然後使用之前要先把 id-creator 的對應關係註冊,而且要在整個程式最開始就註冊,所以要用 static 的方式呼叫。下面是每個要支援的 type 都要註冊。

class Apple : public Fruit
{
    static bool m_bRegistered;
public:
    enum
    {
        ID = 1
    };
    static Fruit* create()
    {
        return new Apple();
    }
};
...
bool Apple::m_bRegistered = FruitFactory2::getInstance()->registerType(Apple::ID, Apple::create);

最後,使用方式跟原本一樣,呼叫 creator 並且指定 id 就可以了。

Fruit* bar = FruitFactory2::getInstance()->create(Apple::ID);

用 Factory template 主要的好處有兩個。第一,每次用到 Factory,只要 typedef 就可以產生一個新的 Factory。第二,同一個 Factory,要加入新的 type 的時候,不必改 Factory 本身,用 add 取代 modify,不會影響舊的功能。

幾個細節問題討論如下。
  1. ID 如何決定則是一個比較傷腦筋的問題,因為 C++ 沒有提供固定的 type id,所以一般是用 hard code 使用字串或者亂數作為索引。甚至,用古老的方式寫 enum 集中管理也是個方式。
  2. 錯誤處理的部份,可能發生 map 找不到的問題,也就是 switch 裡面的 default case,用 policy class 可以選擇不同處理方式。
  3. 最後一個觀察,是 singleton 的部份。前面提到 map 只能有一份,而且 id-creator 必須在程式一開始註冊,所以使用 singleton 是很合理的。這章直接套用 Singleton template 來完成,可以在不同地方提供 Factory 不同的 Singleton 實作,非常的有彈性。

No comments: