1 引言
我的一個實際項目中,由於希望通過一致的接口控制各種型號的設備,並且可以方便的隨時擴充,以便將來支持更多的型號。因此,必須在運行時指定設備的型號。
為了使應用程序可以透明的控制各種型號的設備,所以建立了一個簡單的繼承體系,設計一個協議類(Protocol Class)作為設備的控制接口,並且為每個型號的設備設計了一個具體的類,從協議類派生並且實現了抽象的公共接口。
因此,我需要一種手段,根據設備的型號在運行時動態的創建設備類實例。否則,如果在編譯時硬編碼(Hard Code)設備配置,將失去實用性和靈活性。
最終的結果是,需要這樣一種技術,可以實現
Motor* motor=ClassByName("IM9001");
類似的功能。
2 設計和實現
現有的關鍵類的代碼片斷如下:
class IntelligentMotor
{
public:
IntelligentMotor(const std::string& port_name);
virtual bool Start()=0;
virtual bool Stop()=0;
virtual ~IntelligentMotor();
};
class IM9001 : public IntelligentMotor
{
public:
IM9001(const std::string& port_name);
virtual bool Start();
virtual bool Stop();
virtual ~IM9001();
private:
// ...
};
class IM9002 : public IntelligentMotor
{
public:
IM9002(const std::string& port_name);
virtual bool Start();
virtual bool Stop();
virtual ~IM9002();
private:
// ...
};
// more model ...
如何實現ClassByName呢?
我們當然可以在ClassByName中使用一個多重分支檢查來實現,也就是
IntelligentMotor* ClassByName(const std::string& model_name,
const std::string& port_name)
{
if (model_name=="IM9001")
return new IM9001(port_name);
else if (model_name=="IM9002")
return new IM9002(port_name);
// 所有支持的型號
else
throw "不支持該型號的設備。";
}
然而這種編程風格是糟糕的。隨著系統支持的型號種類增加,分支檢查語句的長度也會同時增加,造成代碼尺寸膨脹和可維護性的下降。
因此必須在類型名字和類(或者類的實例)之間建立一種映射。由於派生類的指針(或引用)可以隱式的轉換成基類的指針(或引用),因此很容易得到這樣的構造:
struct Map
{
const std::string ModelName;
IntelligentMotor* Device;
};
進而我們還可以構造一個型號映射表,並且在編譯時增加所有支持的型號。
Map ModelTable[]=
{
{"IM9001",new IM9001},
{"IM9002",new IM9002}
// 所有支持的型號
};
然後就得到了更清晰的ClassByName。
IntelligentMotot* ClassByName(const std::string& model_name,
const std::string& port_name)
{
for (int i=0;i<sizeof(ModelTable)/sizeof(Map);++i)
{
if (model_name==ModelTable[i].ModelName)
return ModelTable[i].Device;
}
throw "不支持該型號的設備。";
}
然而,在上面我故意忽略了一個問題,設備類(IM9001, IM9002等)並沒有提供默認構造函數,因此實際上這些代碼是無法通過編譯的。可以通過修改接口的設計來避開這個問題,即增加默認構造函數和指定端口的構造函數。雖然這和實際情況不符(因為這裡的設備不支持熱插拔,不能再程序運行時更換連接的端口),但是為了便於實現也是可以接受的。
但是,更隱蔽的一個缺陷是,如果設備類本身的尺寸較大,那麼為所有支持的型號創建實例,將增加空間負荷,而實際上實際使用的設備可能僅僅只用到其中的兩三個型號而已。
從這個角度看,型號映射表中應該映射的是類的創建器,而不是類的實例本身,而類的創建器幾乎沒有什麼空間負擔。
這裡有一個極其簡單的創建器,
template <class T>
IntelligentMotor* IntelligentMotorCreator(const std::string& port_name)
{
return new T(port_name);
}
現在我們的映射表變成了
typedef IntelligentMotor* (*Creator)(const std::string& port_name);
struct Map
{
const std::string ModelName;
Creator DeviceCreator;
};
Map model_table[]=
{
{"IM9001",&(IntelligentMotorCreator<IM9001>)},
{"IM9002",&(IntelligentMotorCreator<IM9002>)}
//...
};
而ClassByName則變成了
IntelligentMotor* ClassByName(const std::string& model_name,
const std::string& port_name)
{
for (int i=0;i<sizeof(model_table)/sizeof(Map);++i)
{
if (model_name==model_table[i].ModelName)
return (*model_table[i].DeviceCreator)
(port_name);
}
throw "不支持該型號的設備。";
}
3 擴充
我們現在有了實用的ClassByName,但是還有幾個小問題期待我們去解決。
首先,如果查找成為一種費時的操作,那麼我們可以使用一個關聯數組代替內建數組提供類型查找功能。也就是
typedef std::map<std::string,Creator> ClassLookupTable;
ClassLookupTable model_lookup_table;
for (int i=0;i<sizeof(ModelTable)/sizeof(Map);++i)
model_lookup_table[ModelTable[i].ModelName]
=ModelTable[i].DeviceCreator;
這時我們的最新版的ClassByName則擁有了略低的平均復雜度。
IntelligentMotor* ClassByName(const std::string& model_name,
const std::string& port_name)
{
ClassLookupTable::const_iterator pos;
pos=model_lookup_table.find(model_name);
if (pos==model_lookup_table.end())
throw "不支持該型號的設備。";
return (*pos->second)(port_name);
}
不過這裡有一個設計上的決策需要使用者作出,因為這個版本需要更多的時間和空間建立快速查找表,這是一種“一次付出,多次分期攤還成本”的優化策略,必須根據實際情況決定。
其次,為了減低編譯依賴性,可以將所有的代碼封裝成庫(或者是共享庫,如動態鏈接庫DLL),只輸出一個ClassByName接口,並單獨提供協議類IntelligentMotor的接口,這樣只要不改變協議類的接口,那麼使用ClassByName的代碼不需要因為增加新型號的設備而重新編譯,如果是共享庫的形式,那麼甚至重新鏈接都不需要,只需要重新編譯獨立的設備型號庫即可。
無論如何,還是必須手工的為支持每一個新的型號而需要手工修改類型創建器的表格。我實在無法使用模板來處理這種異常古怪的情況,不管是內建數組,還是標准容器,都無法處理異種類型混合的元素,只能使用基類指針或者所謂的泛型指針(void *),從類型安全性來說,向上轉型(up-cast)的基類指針更為合適。這就隱含了一個限制,如果是一個任意類型的類型庫系統,則必須要求建立嚴格的單根集成體系,然而據我所知,不管是Java還是VCL,都同樣對此做出了限制,MFC似乎使用了宏,但是我不確定是否克服了這個挑戰。
更一般化的動態創建對象技術,可以閱讀Andrei Alexandrescu的<<Modern C++ Design>>一書,其中第八章Object Factory介紹了完整的動態創建對象的方法。從基本原理上來說,其中使用的技術和本文中的相類似,然而此書中的實現更加靈活、高效和通用。
4 應用
正如前面提起過的,本文介紹的技術,可以用於根據類型名稱動態的創建該類型的實例。
典型的,這種動態創建類實例的方法,是構造可存儲的對象庫(IO object libaray)的常用方法,希望在自己開發的應用框架中提供對象持久性(Persistance)的用戶,可以參考本文的技術。