2016年10月24日 星期一

C++: Prototype Pattern 應用

以前在幸福企業 M 社時,對流程、產品編碼的管理可說到了走火入魔的程度,還為此建立了專業部門。還記得該部門的女同事在教育訓練中狂電台下負責開發產品的男工程師,這些產品編碼搞得比工程師的程式還複雜,也真是強強強~

很不幸的這些編碼也流竄到產品的程式碼中,當年小弟只能照虎畫貓,最近終於找到一種比較好的作法,相信很多人也被這樣的問題困擾,讓我們繼續往下看。


依據產品 ID 產生物件


相信您要不是見過就是寫過類似下方的程式碼:
#include "product_a.h"
#include "product_b.h"
#include "product_c.h"
#include "product_factory.h"


Product* ProductFactory(int id)
{
    Product* product;
    switch(id)
    {
        case PRODUCT_A_ID:
            product = new ProductA;
            break;
        case PRODUCT_B_ID:
            product = new ProductB;
            break;
        case PRODUCT_C_ID:
            product = new ProductC;
            break;
        default:
            product = 0;
    }
    
    return product;
}
這樣的作法只要每次增加新產品,就要來這邊修改 switch case 並且要  include 正確的 header file,而這樣的作法明顯違反 OCP (Open-Closed Principle)

程式碼就是我們的小孩,我們不能讓管理中心製造的各種神秘編碼污染小孩的 DNA。解決辦法就是建立一道防火牆,需要認識編碼的只有該物件本身,並且不使用 switch case id 產生物件,而由物件自己施展影分身之術:
//class Product(interface class)
class Product
{
public:
    virtual Product(){}
    int id()const=0;
    Product* clone()const=0;
};

//class Product A
class ProductA : public Product
{
public:
    Product();
    virtual ~Product();
    int id()const=0;
    ProductA* clone()const=0;
};

//...

ProductA::ProductA(){}
ProductA::~ProductA(){}

int ProductA::id()const
{
    return PRODUCT_A_ID;
}

ProductA* ProductA::clone()
{
    return new ProductA;
}

//class Product B
class ProductB : public Product
{
public:
    Product();
    virtual ~Product();
    int id()const=0;
    ProductB* clone()const=0;
};

//...

ProductB::ProductB(){}
ProductB::~ProductB(){}

int ProductB::id()const
{
    return PRODUCT_B_ID;
}

ProductB* ProductB::clone()
{
    return new ProductB;
}
注意上方的 clone(line:30,55),在 class Product 返回 Product*,在 class ProductA 返回 ProductA*,這在 C++ 裡稱為 co-variant:如果繼承者與被繼承者 virtual function 返回的物件指標或參考,其指向的物件有繼承關係,C++ 則允許返回值不同。

現在還剩下一個問題就是何處呼叫這些 clone(),最好的辦法就是建立一個工廠把自己註冊進去,工廠就知道要如何生產產品了:
//product_factory.h
#include <map>

class Product;
class ProductFactory
{
    ProductFactory();
public:
    ~ProductFactory(){}
    void registerProduct(Product*);
    Product* create(int id);
    static ProductFactory* instance();
    
    
private:
    typedef std::map<int> Map;
    Map map_;
    static ProductFactory *inst_;
};

//product_factory.cpp
#include "product.h"
#include "product_factory.h"

ProductFactory* ProductFactory::inst_ = 0;

ProductFactory* ProductFactory::instance()
{
    if(!inst_)
        inst_ = new ProductFactory;
    
    return inst_;
}

void ProductFactory::registerProduct(Product *p)
{
    if(map_.count(p->id()))
        return;
    
    map_[p->id] = p;
}

Product* ProductFactory::create(int id)
{
    Map::iterator it = map_.find(id);
    if(it != ma_.end())
        return it->second->clone();
    else
        return 0;
}

//product_a.cpp(class ProductA)
static ProductA *RegisterProductA = new ProductA;

ProductA::ProductA()
{
    ProductFactory::instance()->registerProduct(this);
}

藉由靜態指標的初始化,ProductA,B 會在程式開始之初把自己註冊到 ProdctFactory,而後續只要透過 ProductFactory::create() 就可以產生對應物件,由這裡我們可以看到 switch case 被驅逐出境了,成功減少相依性與編碼污染。

而 ProductFactory::instance() 已在前面的文章分享過,這邊不再重複。

改善 State Pattern 設計

能不能靈活運用 libeventlibuv 的秘奧義就是 State Pattern,這是處理 event loop 的關鍵:


這邊就不對 State Pattern 做詳細介紹了,請自行參照四人幫第五章,由上圖可以看出 ConcreteStateA、ConcreteStateB...會依據狀態的複雜度而不斷增長。麻煩的是不同狀態間的轉換,小弟試過不少辦法包含很少人用的 RTTI,後來發現還是簡單的 enum 最好用,用列舉值作為 state id 切換狀態,但這些 id 要怎麼對應到 ConcreteStateA、ConcreteStateB...?

早些年免不了使用 switch case,現在我們有了 Prototype Pattern 日子可以輕鬆一點了,不需在 Context 放進一大堆 header files(ConcreteStateXXX)還有 switch case。
//ConcreateStateA implementation
static ConcreateStateA* RegisterConcreateStateA = new ConcreateStateA

ConcreateStateA::ConcreateStateA()
{
    StateFactory::instance()->add(this);
}


ConcreateStateA* ConcreateStateA::clone()
{
    return new ConcreateStateA;
}


StateId ConcreateStateA::id()const()
{
    return kConcreateStateA;
}


//ConcreateStateB implementation
static ConcreateStateB* RegisterConcreateStateB = new ConcreateStateB;

ConcreateStateB::ConcreateStateA()
{
    StateFactory::instance()->add(this);
}


ConcreateStateB* ConcreateStateA::clone()
{
    return new ConcreateStateA;
}


StateId ConcreateStateB::id()const()
{
    return kConcreateStateA;
}
這裡 StateFactory 與之前的 ProductFactory 如出一轍,所以也沒必要介紹了。

結語


重構一書中曾經說過 OOP 很少使用 switch case,最近才有了些許的領悟,在此分享給各位,謝謝大家~

參考資料: C++ Gotchas: Avoiding Common Problems in Coding and Design

沒有留言:

張貼留言