在C++新增template功能的初期,大家普遍認為template只是為物件導向設計得到更好的重用性,不單單將重複使用的程式碼設計成物件類別(class),連類別也重用,經由編譯器幫programmer把不同型別但功能完全相同的程式碼複製。如standard template library裡的vector,宣告vector就是一個存intergerarray的向量,宣告vector就是一個存character pointer array的向量,各自均在insert, earse等功能相同的介面。
其後有意無意間大家發現template能做的事情不單單如此,甚至出乎意料之外的強大,如使用LISP之類的技巧令編譯器幫你在編譯期運算,或編譯期才組合產生你所需要的程式碼。
在大型長期開發的軟體中,保持
架構、
效能、
延展性和
介面一致十分重要,可是軟體設計必定不斷變動,功能的擴充、模組的更換尤其繁甚,為了加插功能、修改模組,結果往往就是架構變型,效能降低,介面不一致,元件肥大。(更可怕的是為求擴充,不斷新增介面,混亂不堪,日後維護困難,難以接手)
Policy-Based Class Design是一種新思維的設計方法,藉由template把各種小行為在編譯期組合成為功能複雜的元件,具有高度的效能和彈性。這種設計分為兩部份 ── policies, host class。
policies是一些小型的類別,只單純負責某一核心功能,獨立運作的「策略」,如設計模式 (Design Pattern) 中的行為模式 (behavioral pattern) 或結構模式 (structural pattern),而
host class像是一個外殼,由多個policies組成,針對特定主題,定義通用介面和錯綜複雜的邏輯流程,核心功能的細節完全經由組合的policies來實現。這種程式設計就像現實生活中的大公司,主管就是host class,為了完成專案找了幾個小將(policies)回來,排行程,分配資源,協調工作,開會擋箭,追殺廠商等,小將只要完成專案裡的需求就好了。(真完美)
現在來看看軟體裡經常會遇到的案例,假設軟體需要存儲設定,一開始決定使用ini格式,因此普遍會寫一個ConfigManager,提供寫入、擷取和操作介面。
class ConfigManager {
public:
BOOL Load(char* path);
void Write(char* path);
BOOL Add(char* columeName, char* columeValue);
char* columeValue Query(char* columeName);
};
如果考慮到延展性,ConfigManager將會繼承Load()和Write()這兩個單獨功能的類別。但這樣有一個缺點ConfigManager會繼承IniLoader和IniWriter裡所有公開介面,介面被破壞。
class ConfigManager : public IniLoader, public IniWriter {
public:
BOOL Add(char* columeName, char* columeValue);
char* columeValue Query(char* columeName);
};
class IniLoader {
public:
virtual BOOL Load(char* path);
};
class IniWriter {
public:
virtual BOOL Write(char* path);
};
除了使用多重繼承外,也可使用組合物件方式。
class ConfigManager {
public:
BOOL Load(char* path) {
if (IsExist(path))
m_iniLoader.Load(path);
}
void Write(char* path) {
if (IsExist(path))
m_iniWriter.Write(path);
}
BOOL Add(char* columeName, char* columeValue);
char* columeValue Query(char* columeName);
private:
IniLoader m_iniLoader;
IniWriter m_iniWriter;
};
其後軟體需要更換模組,假設由Ini改為Xml,還需要自動從Ini升級到Xml喔。所以可能會設計抽象類別BaseLoader和BaseWriter,IniLoader, XmlLoader, IniWriter, XmlWriter分別繼承抽象,並傳入ConfigManager中,讓上層去決定使用那種Loader和Writer。
class ConfigManager {
public:
ConfigManager(BaseLoader* pLoader, BaseWriter* pWriter) {
m_pLoader = pLoader; m_pWriter = pWriter;
}
BOOL Load(char* path) {
if (IsExist(path))
m_iniLoader.Load(path);
}
void Write(char* path) {
if (IsExist(path))
m_iniWriter.Write(path);
}
BOOL Add(char* columeName, char* columeValue);
char* columeValue Query(char* columeName);
private:
BaseLoader* m_pLoader;
BaseWriter* m_pWriter;
};
class BaseLoader {
public:
virtual BOOL Load(char* path) = 0;
};
class BaseWriter {
public:
virtual BOOL Write(char* path) = 0;
};
現在的設計看起來一切都很好,可怕的事情發生了,針對Xml的讀入處理,ConfigManager需要提供一個介面Fix(),這時介面的一致性就被破壞了,如果使用一開始所說的多重繼承,則必須宣告四個不同的類別才能令介面保持一致性。如果未來還新增網路讀取功能NetLoader, NetWriter,就必須宣告 2 * 2 * 2 個不同類別了,這不就正好是template的能力嗎。
”令軟體存在數種不同版本的設計實作方案,每次只從中選擇其一的方案,而又需要保持切換與擴充的彈性”就是Policy-Based Class Design的中心思想。Policy-Based Class Design的設計如下,ConfigManager是host class,IniLoader, IniWriter, XmlLoader和XmlWriter是policies。
template<class Loader, class Writer>
class ConfigManager : public Loader, public Writer {
public:
BOOL Load(char* path) {
if (IsExist(path))
m_iniLoader.Load(path);
}
void Write(char* path) {
if (IsExist(path))
m_iniWriter.Write(path);
}
BOOL Add(char* columeName, char* columeValue);
char* columeValue Query(char* columeName);
};
class XmlLoader {
public:
BOOL Load(char* path);
BOOL Fix();
};
void main() {
ConfigManager<Xmlloader, Iniwriter> c1;
ConfigManager<Iniloader, Xmlwriter> c2;
ConfigManager<Xmlloader, Netwriter> c3;
c1.Load("xxx");
c1.Fix();
c2.Load("xxx");
c2.Fix(); // compile error
c3.Load("xxx");
c1.Write("xxx");
c2.Write("xxx");
c3.Write("xxx");
}
看到這裡總會有點點卡住,到底Polymorphism Design和Policy-Based Class Design有甚麼差別呢。其實兩者之間有著極大的差異,如果說繼承體系是上而下 (top down)構成,介面由Base class定義,由Derived class實作,那麼Policy-Based Class就是下而上 (bottom up)構成,介面由Host class定義,由Policy class實作。Host class會繼承它所需的policies,並且在Host class中定義出操作行為的骨架流程,至於真正的實作細節,則全權委派 (delegate)給policies進行處理。
架構恰好完全與繼承體系方向相反。
上述例子中還不算最終形式的Policy-Based Class Design,就像遞迴一樣的概念,tempate也可以是多層的,經由Host class傳遞template parameter給polices來逹到,能組合出更多的功能。(這種組合可是次方級數成長的喔),如下,現在不再統一一起寫設定檔,改為每個元件單獨讀寫。
template<class Loader, class Writer, class Componet>
class ConfigManager : public Loader<Componet>, public Writer<Componet> {
public:
BOOL Load(char* path) {
if (IsExist(path))
m_iniLoader.Load(path);
}
void Write(char* path) {
if (IsExist(path))
m_iniWriter.Write(path);
}
BOOL Add(char* columeName, char* columeValue);
char* columeValue Query(char* columeName);
};
template<class Componet>
class IniLoader {
public:
BOOL Load(char* path) {
Componet::GetConfigInfo();
// do something
}
};
void main() {
ConfigManager<XmlLoader, IniWriter, Schedular> c1;
ConfigManager<IniLoader, XmlWriter, Recorder> c2;
ConfigManager<IniLoader, NetWriter, RemoteServer> c3;
c1.Load("xxx");
c1.Fix();
c2.Load("xxx");
c2.Fix(); // compile error
c3.Load("xxx");
c1.Write("xxx");
c2.Write("xxx");
c3.Write("xxx");
}
說穿了Policy-Based Class Design的基本原理就是Strategy Pattern,如果現在需要的策略只有一個,那整個Host class就跟Strategy Pattern十分類似,也沒有必要使用Policy Design了。但是,如果現在需要的策略是兩個以上,還繼續使用Strategy Pattern反而會降低效能,元件肥大,日後難以維護。而Policy-Based Class Design就是使用template幫你組合程式碼,把多個Strategy Pattern組合成為一個類別,同時減低設計的相依性。
總結一下Policy-Based Class Design的優缺處
1. 高顆粒性 (granularity),核心功能獨立運作,方便unit test。
2. 高延展性。只要架構不變,新增功能、更換模組和小修改,只要寫Policy class就可以了。
3. 高擴充性。這是進階主題,下回再說。
4. 介面一致。每一個ConfigManger有一致的共用介面,定義在Host class裡,同時,對於特定某種ConfigManager能從繼承中獲得額外的介面,別與其他ConfigManager,又能保持介面完整。
5. 介面高彈性。這是進階主題,下回再說。
6. 高效能。每一個ConfigManager的介面和實作由繼承而來,避免透過成員變數中的抽象類別間接呼叫。值得注意的是Host, Policy class基本上是不用(也不需要)虛擬函式,直接免除virtual table的間接呼叫和記憶體使用量。
7. Host class實作技巧高。Host class使用到template,比直接寫一般的C++難。
8. 實作思想需要改變。Policy class非常獨立,常常是沒有任何成員變數,只實作行為,資料由參數傳入,回傳結果。