模板方法模式

模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。

../../_images/template-method.png

问题

假如你正在开发一款分析公司文档的数据挖掘程序。 用户需要向程序输入各种格式 (PDF、DOC 或 CSV) 的文档, 程序则会试图从这些文件中抽取有意义的数据, 并以统一的格式将其返回给用户。

该程序的首个版本仅支持 DOC 文件。 在接下来的一个版本中, 程序能够支持 CSV 文件。 一个月后, 你 “教会” 了程序从 PDF 文件中抽取数据。

../../_images/data_problem.png

一段时间后, 你发现这三个类中包含许多相似代码。 尽管这些类处理不同数据格式的代码完全不同, 但数据处理和分析的代码却几乎完全一样。 如果能在保持算法结构完整的情况下去除重复代码, 这难道不是一件很棒的事情吗?

还有另一个与使用这些类的客户端代码相关的问题: 客户端代码中包含许多条件语句, 以根据不同的处理对象类型选择合适的处理过程。 如果所有处理数据的类都拥有相同的接口或基类, 那么你就可以去除客户端代码中的条件语句, 转而使用多态机制来在处理对象上调用函数。

解决方案

模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。 步骤可以是 抽象的, 也可以有一些默认的实现。 为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。 如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。

让我们考虑如何在数据挖掘应用中实现上述方案。 我们可为图中的三个解析算法创建一个基类, 该类将定义调用了一系列不同文档处理步骤的模板方法。

../../_images/data_solution.png

首先, 我们将所有步骤声明为 抽象类型, 强制要求子类自行实现这些方法。 在我们的例子中, 子类中已有所有必要的实现, 因此我们只需调整这些方法的签名, 使之与超类的方法匹配即可。

现在, 让我们看看如何去除重复代码。 对于不同的数据格式, 打开和关闭文件以及抽取和解析数据的代码都不同, 因此无需修改这些方法。 但分析原始数据和生成报告等其他步骤的实现方式非常相似, 因此可将其提取到基类中, 以让子类共享这些代码。

正如你所看到的那样, 我们有两种类型的步骤:

  • 抽象步骤必须由各个子类来实现
  • 可选步骤已有一些默认实现, 但仍可在需要时进行重写

还有另一种名为钩子的步骤。 钩子是内容为空的可选步骤。 即使不重写钩子, 模板方法也能工作。 钩子通常放置在算法重要步骤的前后, 为子类提供额外的算法扩展点。

模板方法模式结构

../../_images/template_method_structure.png

  • 抽象类 (Abstract­Class) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为 抽象类型, 也可以提供一些默认实现。
  • 具体类 (Concrete­Class) 可以重写所有步骤, 但不能重写模板方法自身。

真实世界类比

../../_images/template_method_real_world.png

模板方法可用于建造大量房屋。 标准房屋建造方案中可提供几个扩展点, 允许潜在房屋业主调整成品房屋的部分细节。

每个建造步骤 (例如打地基、 建造框架、 建造墙壁和安装水电管线等) 都能进行微调, 这使得成品房屋会略有不同。

代码示例

//@ 公司
class Company
{
public:
	virtual ~Company() {}

	//@ 校园招聘
	void Recruit() {
		std::cout << "---------- Begin ----------" << std::endl;
		careerTalk();
		receiveResume();
		interview();
		offer();
		std::cout << "---------- End ----------" << std::endl;
	}

	//@ 宣讲会
	void careerTalk() {
		std::cout << "Delivery" << std::endl;
	}

	//@ 接收简历
	void receiveResume() {
		std::cout << "Receive Resume" << std::endl;
	}

	//@ 面试
	virtual void interview() = 0;

	//@ 发放 offer
	virtual void offer() = 0;
};

//@ 阿里
class Alibaba : public Company
{
public:
	virtual void interview() override {
		std::cout << "First interview -> Second interview -> Third interview" << std::endl;
	}

	virtual void offer() override {
		std::cout << "30W" << std::endl;
	}
};

//@ 腾讯
class Tencent : public Company
{
public:
	virtual void interview() override {
		std::cout << "First interview -> Second interview" << std::endl;
	}

	virtual void offer() override {
		std::cout << "25W" << std::endl;
	}
};

int main()
{
	//@ 阿里校招
	std::shared_ptr<Company> alibaba(new Alibaba());
	alibaba->Recruit();

	//@ 腾讯校招
	std::shared_ptr<Company> tencent(new Tencent());
	tencent->Recruit();

	return 0;
}

模板方法模式总结

实现方式

  • 分析目标算法, 确定能否将其分解为多个步骤。 从所有子类的角度出发, 考虑哪些步骤能够通用, 哪些步骤各不相同。
  • 创建抽象基类并声明一个模板方法和代表算法步骤的一系列抽象方法。 在模板方法中根据算法结构依次调用相应步骤。 可用 final最终修饰模板方法以防止子类对其进行重写。
  • 虽然可将所有步骤全都设为抽象类型, 但默认实现可能会给部分步骤带来好处, 因为子类无需实现那些方法。
  • 可考虑在算法的关键步骤之间添加钩子。
  • 为每个算法变体新建一个具体子类, 它必须实现所有的抽象步骤, 也可以重写部分可选步骤。

优点

  • 可仅允许客户端重写一个大型算法中的特定部分, 使得算法其他部分修改对其所造成的影响减小。
  • 可将重复代码提取到一个超类中。

缺点

  • 部分客户端可能会受到算法框架的限制。
  • 通过子类抑制默认步骤实现可能会导致违反_里氏替换原则_。
  • 模板方法中的步骤越多, 其维护工作就可能会越困难。

适用场景

  • 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即:一次性实现一个算法的不变部分,并将可变的行为留给子类来实现。
  • 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。