很不幸的這些編碼也流竄到產品的程式碼中,當年小弟只能照虎畫貓,最近終於找到一種比較好的作法,相信很多人也被這樣的問題困擾,讓我們繼續往下看。
依據產品 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
沒有留言:
張貼留言