Apr 23 2008
物件導向設計新思維:探索Policy-Based Class Design新視界
Policy-Based Class Design 到底是什麼樣的東西?又能夠用來解決什麼樣的問題?本文將由一個在程式專案中常見的現實案例,帶領讀者進入 Policy-Based Class Design 的全新思維領域。
學習一項新的思維方法或實作技術,最容易的方法就是從需求面出發。所以在介紹 Policy-Based Design 之前,先來看看一個現實中經常會遇到的程式設計案例:假設在處理 Input/Output 功能的系統層,原來存在著 BinaryReader 與 BinaryWriter 兩個類別,分別用來讀取以及寫入二進位制的資料串流。
現在,在新的專案項目中,我們希望能夠利用這兩個既存的 IO 類別,但是又不希望將檔案的讀取與寫入動作,分成 BinaryReader 與 BinaryWriter 兩個不同的類別進行處理。因此,程式設計者需要設計一個新的 IO 管理者類別,將原有的讀取與寫入功能合併在一起,以達到集中操作以及檔案資源管理的功用。
而為了盡量減少對既存程式碼的修改,其中一種設計方法是使新的 IOManager 類別,同時繼承自 BinaryReader 與 BinaryWriter 類別,然後藉由虛擬函式的宣告,修改專案所需的行為程序:
class IOManager
:
public BinaryReader,
public BinaryWriter
{
public:
virtual void Read(); // 覆寫自 BinaryReader
virtual void Write(); // 覆寫自 BinaryWriter
}
這樣設計的方法確實能夠在不修改原有類別的前提下,達到 IO 系統介面集中的目的。程式系統的撰寫者無須再同時照料 BinaryReader 與 BinaryWriter 兩個類別,只要一個 IOManager 類別就能夠處理 IO 層面的功能操作。
而除了前例所使用的多重繼承 (Multiple Inheritance) 以外,另外一種設計方法則是使用物件複合 (Object Composition) 的方式,將 BinaryReader 與 BinaryWriter 類別的物件當做成員變數包含在 IOManager 類別中:
class IOManager
{
public:
void Read()
{
pkReader->Read();
}
void Write()
{
pkWriter->Write();
}
private:
BinaryReader* m_pkReader;
BinaryWriter* m_pkWriter;
}
為了將 BinaryReader 與 BinaryWriter 類別所提供的功能當作黑盒子組件使用,不論是多重繼承或者物件複合,都是很常見的合理作法。然而,如果在專案開發過程中提出了新的設計需求,需要讀取其他格式的檔案,例如 XML 檔案時,應該要如何整合至 IOManager 類別中?
在程式系統的設計中,大部分的情況下我們會希望在管理者類別中,能夠盡量保持一致的使用介面,例如 IOManager 類別中的 Read() 與 Write() 函式,對於 IO 系統的使用者來說,並不需要去瞭解目前讀取的是 Binary 格式或 XML 格式的資料,只需要直接使用 Read() 與 Write() 函式就能夠達到讀取與寫入檔案的目標。
為了完成這項新的設計需求,程式設計者需要依照前述的作法實作出 XmlReader 與 XmlWriter 類別之後,再以多重繼承或物件複合的方法創建出新的 XmlIOManager 類別:
class XmlIOManager
:
public XmlReader,
public XmlWriter
{
public:
virtual void Read(); // 覆寫自 XmlReader
virtual void Write(); // 覆寫自 XmlWriter
}
如果需要讀取或寫入 Binary 格式的檔案就使用原來的 IOManager;而如果需要讀取或寫入 XML 格式的檔案,就利用新的 XmlIOManager 類別。OK,搞定收工!
等等,如果假設萬一有可能(就是真的會發生)專案需要以 XML 格式讀取檔案,同時使用 Binary 格式寫入檔案時,應當如何是好?同樣地,我們可以使用物件複合的方式,創建一個新的(名字很長的) XmlInputBinaryOutputManager 類別:
class XmlInputBinaryOutputManager
{
public:
void Read()
{
pkReader->Read();
}
void Write()
{
pkWriter->Write();
}
private:
XmlReader* m_pkReader;
BinaryWriter* m_pkWriter;
}
問題是解決了,但是按照這樣的程式設計與實作方法,很快地就會出現 BinaryInputBinaryOutputManager、XmlInputXmlOutputManager、XmlInputBinaryOutputManager、BinaryInputXmlOutputManager 四個高階功能介面相同(名字同樣很長),但實作細節不同的 IOManager 類別家族。
另外一個比較理想的解決方案,應該是新建一個抽象的 BaseReader 類別以及一個抽象的 BaseWriter 類別,使 BinaryReader 與 XmlReader 衍生自 BaseReader 類別,BinaryWriter 與 XmlWriter 衍生自 BaseWriter 類別,然後在建構 IOManager 類別時,傳入所需的實作版本:
class IOManager
{
public:
IOManager(BaseReader* pkReader, BaseWriter* pkWriter)
{
m_pkReader = pkReader;
m_pkWriter = pkWriter;
}
void Read()
{
pkReader->Read();
}
void Write()
{
pkWriter->Write();
}
private:
BaseReader* m_pkReader;
BaseWriter* m_pkWriter;
}
不是很直覺的使用方式,但還算是個能夠接受的解決方案。只是,做為程式設計者,是否只能屈服於這樣的設計方法?是否還有其他的可能性存在?
其實,在上述的程式設計案例中,已經點出了一個在專案開發流程中常見的情形:存在數種不同版本的設計實作方案,每次只從中選擇其一的方案,但是又需要保持切換與擴充的彈性。設計,本身就是一種不斷變動的行為。設計者經常會遇到兩難的情境:一方面既想保持系統的修改彈性,另一方面又要能夠達到理想的執行效能與成果。
在《C++ 設計新思維》(Modern C++ Design) 中的第一章,作者 Andrei Alexandrescu 為人們展示了一種全新的 Template 程式設計技術,稱之為 Policy-Based Class Design。Policy-Based Class Design 同時使用了 Template 以及 Multiple Inheritance 兩項技術,結合兩者的優點,成為一種既新穎又實用的設計方法。Policy,字面上的意思是:政策、方針,或策略。所以 Policy-Based Class Design 也就是將原來錯綜複雜的程式系統,拆解成為數個獨立運作的「策略」,然後藉由 Policy 之間的分工合作與交替組合能力,使得以這個設計架構完成的程式系統能夠同時得到擴充性、彈性與易用性等多項優點。
如果使用 Policy-Based Design 方法,重新改寫上述的類別設計範例,可以得到以下的初步結果:
template
<
class ReadingPolicy,
class WritingPolicy
>
class ResourceIOManager
:
public ReadingPolicy,
public WritingPolicy
{
public:
void Read()
{
ReadingPolicy::Read();
}
void Write()
{
WritingPolicy::Write();
}
};
請特別注意在這段程式碼中,同時運用 Template 與 Multiple Inheritance 的作法。在 Template 中所使用的 ReadingPolicy 與 WritingPolicy 類別,是程式設計者能夠在編譯時期自行決定的一種類別參數,再加上 ResourceIOManager 類別繼承自這兩個當作 Template 建構參數傳入的 ReadingPolicy 與 WritingPolicy 類別,所以在 Read() 與 Write() 函式中,就可以直接呼叫並且委託這兩個 Policy 類別進行讀取與寫入的程序。
而上述兩項需要傳入做為 Template 建構參數的 ReadingPolicy 與 WritingPolicy 類別是什麼?其實也就是之前已經存在的 BinaryReader、BinaryWriter、XmlReader 以及 XmlWriter 類別:
class BinaryReader
{
public:
void Read()
{
// Reading in binary mode
}
};
class BinaryWriter
{
public:
void Write()
{
// Writing in binary mode
}
};
在 Policy-Base Class Design 中,ResourceIOManager 類別所扮演的角色為 Host Class,而 BinaryReader 與 BinaryWriter 這些不同版本的 Reader 與 Writer 實作類別則稱為 Policy Class。只需要切換不同 Policy Class,程式設計者就能夠輕鬆地組合出完全合乎設計需求的類別功能:
void main()
{
// Binary read, binary write
ResourceIOManager< BinaryReader, BinaryWriter > kBinaryIOMgr;
kBinaryIOMgr.Read();
kBinaryIOMgr.Write();
// Xml read, Xml write
ResourceIOManager< XmlReader, XmlWriter > kXmlIOMgr;
kXmlIOMgr.Read();
kXmlIOMgr.Write();
// Xml read, Binary write
ResourceIOManager< XmlReader, BinaryWriter > kXmlBinaryIOMgr;
kXmlBinaryIOMgr.Read();
kXmlBinaryIOMgr.Write();
}
如上述的使用範例所示,只需要一個 ResourceIOManager 類別,以及四個 Reader 與 Writer 類別,就能夠自由地組合出四種不同實作版本的 IO 程序處理器!未來如果需要加入處理純文字檔案用的 TextReader 與 TextWriter,只要利用相同的方法,同樣能夠輕易地達到擴充實作版本的目標。
然而,上述的實作方法,仍然不是 Policy-Based Design 的正確形式,還沒有將 Policy-Based Design 的威力發揮到極致。在前例中,建構 ResourceIOManager 類別所使用的 BinaryReader 等四個 Policy Class,都只是一般的類別。Policy-Based Design 真正厲害之處在於,這些用來建構 Template 的類別參數,本身亦可以是一個 Tempate 化的類別!
也就是說,我們可以將 BinaryReader 與 BinaryWriter 修改成為 Templated Class:
template< class T >
class BinaryReader
{
public:
void Read()
{
// Reading in binary mode
}
};
template< class T >
class BinaryWriter
{
public:
void Write()
{
// Writing in binary mode
}
};
接著,將 ResourceIOManager 修改為接受 Templated Classes 做為建構參數的類別:
template
<
class Subject,
template< class > class ReadingPolicy,
template< class > class WritingPolicy
>
class ResourceIOManager
:
public ReadingPolicy< Subject >,
public WritingPolicy< Subject >
{
public:
void Read()
{
ReadingPolicy< Subject >::Read();
}
void Write()
{
WritingPolicy< Subject >::Write();
}
};
請注意上述程式碼中 template< class > class ReadingPolicy 以及 template< class > class WritingPolicy 的用法。這樣的程式碼寫法,宣告了傳入 ResourceIOManager 類別的 ReadingPolicy 與 WritingPolicy 類別參數,本身也是一個 Template 化的類別,需要再傳入另一個類別做為兩者的建構參數。而在此,就是由其中的 Subject 類別,扮演這個 Template Template Parameter(樣版的樣版參數)的角色。
這個 Subject 類別,做為建構實體物件用的參數傳入 ResourceIOManager 類別,同時也做為 ReadingPolicy 以及 WritingPolicy 類別的建構參數。有了 Subject 類別參數之後,就能夠使 Policy Class 更具擴充性與彈性,能夠處理其他類型的實體檔案,例如 Animation、Texture 或 Script 資源等等:
void main()
{
// 管理 Animation 檔案資源
ResourceIOManager< AnimationResource, BinaryReader, BinaryWriter > kMgr1;
kMgr1.Read();
kMgr1.Write();
// 管理 Texture 檔案資源
ResourceIOManager< TextureResource, XMLReader, XMLWriter > kMgr2;
kMgr2.Read();
kMgr2.Write();
// 管理 Script 檔案資源
ResourceIOManager< ScriptResource, TextReader, TextWriter > kMgr3;
kMgr3.Read();
kMgr3.Write();
}
以上的實作方式,才是 Policy-Based Class Design 真正的設計形式以及使用方法。至此,原先存在的 BinaryReader、BinaryWriter、XMLReader 與 XMLWriter 類別,不再需要綁死於單一的 IOManager 管理者類別之中,而能夠自由組合替換,甚至對不同類型的檔案資源做出特定的處理程序,也完成了兼具修改彈性與執行效能的系統架構設計目標。
更進一步,如果需要在 ResourceIOManager 類別中再加入新的 Policy Class,例如用來處理錯誤狀態的 ErrorHandlingPolicy,同樣也非常容易:
template
<
class Subject,
template< class > class ReadingPolicy,
template< class > class WritingPolicy,
template< class > class ErrorHandlingPolicy
>
class ResourceIOManager
:
public ReadingPolicy< Subject >,
public WritingPolicy< Subject >,
public ErrorHandlingPolicy< Subject >
{
public:
void Read()
{
if (!ReadingPolicy< Subject >::Read())
{
ErrorHandlingPolicy< Subject >::HandleError();
}
}
void Write()
{
if (!WritingPolicy< Subject >::Write())
{
ErrorHandlingPolicy< Subject >::HandleError();
}
}
};
在 Policy-Based Class Design 中,可以說整個 Host Class 幾乎就是 Policy Class 們的集合。Host Class 本身不經手功能層面的實作細節,而僅訂立出功能運作的骨架流程,真正的運作細節則交由各個不同的 Policy Class 分工進行處理。在 Wikipedia 中對於 Policy-Based Design 的介紹,有一段很貼切的敘述:
Wherever we would need to make a possible limiting design decision, we should postpone that decision and delegate it to an appropriately named policy.
在程式系統的設計過程中,有時由於時間或資源的因素,所做出的設計決定往往考慮不周全而十分受侷限。因此做為一位程式設計者,應該盡可能延後做出決定的時機,不要過早將設計決定綁死在前期的程式系統設計架構中,才能夠因應未來不斷發生的需求變動以及設計變動,也才能夠真正地達到程式系統組件的可復用性 (Reusability)。也正如《C++ 設計新思維》中所提到的:
「設計」就是一種「選擇」。大多數時候我們的困難並不在於找不到解決方案,而是有太多解決方案。
而 Policy-Based Class Design 正是為此目的而生。藉由既有技術的巧妙結合,Policy-Based Class Design 為程式設計者帶來前所未有的全新思維模式以及實作技術,使程式設計者能夠在許多不同的解決方案中,自由選擇並且輕鬆組合出最合適的實作版本。
在下篇文章裡,我將會更深入詳細地闡述 Policy-Based Class Design 背後的思維模式,以及更多在遊戲程式系統中的相關應用。
程式碼範例下載:PolicyBasedDesign_SampleCode.zip (下載次數: 92 )

也許用設計模式中的狀態模式會比較輕鬆一點?好像從以前就被灌輸觀念不要用多重繼承,當然我想多重繼承還是有它的必要性,不然早就被c++淘汰掉了。
我有一個疑問..
如果XML和Binary的Read()、Write()有參數..但參數不一樣的話,
Manager該怎麼辦才能達成需求?
每次看到這東西都覺得很神奇,但就連叫我clone一個出來我都作不到orz