示例
需求:
數據處理軟件往往需要對錶格內的每一條記錄或者是一組特定的記錄進行操作,執行步驟大概如下:
- 遍歷讀表格,把需要處理的表格置入內存容器
- 處理容器內的表格數據
代碼片斷如下:
// 第一個實現
// 基類(模板類)
class GenericTableAlgorithm
{
public:
GenericTableAlgorithm(const string& table);
virtual ~GenericTableAlgorithm();
// 負責所有工作:
// a. 遍歷表格中所有行,併爲每一行調用Filter()以決定該行是否應該被放置於待處理容器中
// b. 對待處理容器中每一記錄調用ProcessRow()
// 成功返回true
bool Process();
protected:
// 如果是需要處理的數據,默認返回true
virtual bool Filter(const Record&);
// 處理數據的具體行爲,延遲到子類實現
virtual bool ProcessRow(const PrimaryKey&) = 0;
private:
class GenericTableAlgorithmImpl* pimpl_;
};
// 一種具現
class MyAlgorithm : public GenericTableAlgorithm
{
public:
// override...
virtual bool Filter(const Record&) override; // 如果必要
virtual bool ProcessRow(const PrimaryKey&) override; // 必須
private:
//...
};
// 使用
int main(void)
{
MyAlgorithm a("Customer");
a.Process();
}
分析
根據需求,該程序的處理過程遵循一定的步驟。並且步驟確定之後,一般不會有所更改。
而具體的處理細節可能會有較大的不同。
所以,在基類中實現了相同且固定的步驟,細節的處理則由各子類具體實現。這就是模板方法。
這樣既避免了重複的工作,又能把處理細節靈活地延遲到子類去實現,非常好。
但是,這樣的設計有沒有缺點呢?如何改進?
改進
由於基類GenericTableAlgorithm同時做兩份任務,違背了類設計中的單一職責原則,所以可以對該類進行優化。
GenericTableAlgorithm承擔兩個不同且互不相干的任務,可以被有效地隔離,以支撐不同的應用場景。
改進後的效果如下:
// gta.h
// 提供一個公共接口,用來封裝共同的機能,使之成爲一個Template Method
// 不再被繼承,具有單一職責
class GTAClient;
class GenericTableAlgorithm
{
public:
GenericTableAlgorithm(const string& table, GTAClient& worker); // 需要一個客戶的實例
~GenericTableAlgorithm();
bool Process(); // 仍完成核心邏輯過程
private:
class GenericTableAlgorithmImpl* pimpl_;
};
// gtaclient.h
// 爲擴展而提供的抽象接口,是GenericTableAlgorithm的實現細節
// 與外部client無關,集中於抽象,由客戶實現抽象
class GTAClient
{
public:
virtual ~GTAClient() = 0;
virtual bool Filter(const Record&) { return true; }
virtual bool ProcessRow(const PrimaryKey&) = 0;
};
// 客戶實現具體的抽象,與之前的版本非常相似
class MyWorker : public GTAClient
{
public:
// override...
virtual bool Filter(const Record&) override;
virtual bool ProcessRow(const PrimaryKey&) override;
};
int main(void)
{
GenericTableAlgorithm a("Customer", MyWorker());
a.Process();
}
主要優點:
- 如果GenericTableAlgorithm的接口發生變化,如增加一個新的public member,原來的設計中,所有具象的worker class必須重新編譯。而新版本中的更改不會影響到worker class
- 如果Filter()或者ProcessRow()改變了,原版本中所有外部的clients必須重新編譯,因爲它們在client的定義區內,新版本則不會影響外部使用者
- 任何具象的worker現在可以在任何算法中被使用,只要該算法能夠使用Filter和ProcessRow接口,而不只是被GenericTableAlgorithm使用。這,已經類似於策略模式。
其實,到目前爲止,GenericTableAlgorithm類可以改寫爲一個函數:
bool GenericTableAlgorithm(const string& table, GTAClient& worker)
{
// Process()的實現
}
具體何種實現方式,取決於系統需求及擴展需要。
小結
模板模式使用廣泛,可能只是你不知道它的名字,但你已經在項目中使用了它。
應該注意盡力讓每一段代碼、每一個類、每一個函數,都有單一而明確的職責。這是一個持續而永久的任務。
對於特定的需求,稍作改進就能得到這樣一個穩定可複用的模式。
參考資料
《Exceptional C++》