很不幸的這些編碼也流竄到產品的程式碼中,當年小弟只能照虎畫貓,最近終於找到一種比較好的作法,相信很多人也被這樣的問題困擾,讓我們繼續往下看。
依據產品 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 設計
這邊就不對 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
參考資料: C++ Gotchas: Avoiding Common Problems in Coding and Design

沒有留言:
張貼留言