示例
需求:
数据处理软件往往需要对表格内的每一条记录或者是一组特定的记录进行操作,执行步骤大概如下:
- 遍历读表格,把需要处理的表格置入内存容器
- 处理容器内的表格数据
代码片断如下:
// 第一个实现
// 基类(模板类)
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++》