意图

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

问题

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

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

数据挖掘类中包含许多重复代码

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

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

解决方案

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

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

模板方法将算法分解为步骤,并允许子类重写这些步骤,而非重写实际的模板方法

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

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

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

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

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

结构

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

实现方式

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

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
using System;

namespace RefactoringGuru.DesignPatterns.TemplateMethod.Conceptual
{
// The Abstract Class defines a template method that contains a skeleton of
// some algorithm, composed of calls to (usually) abstract primitive
// operations.
//
// Concrete subclasses should implement these operations, but leave the
// template method itself intact.
abstract class AbstractClass
{
// The template method defines the skeleton of an algorithm.
public void TemplateMethod()
{
this.BaseOperation1();
this.RequiredOperations1();
this.BaseOperation2();
this.Hook1();
this.RequiredOperation2();
this.BaseOperation3();
this.Hook2();
}

// These operations already have implementations.
protected void BaseOperation1()
{
Console.WriteLine("AbstractClass says: I am doing the bulk of the work");
}

protected void BaseOperation2()
{
Console.WriteLine("AbstractClass says: But I let subclasses override some operations");
}

protected void BaseOperation3()
{
Console.WriteLine("AbstractClass says: But I am doing the bulk of the work anyway");
}

// These operations have to be implemented in subclasses.
protected abstract void RequiredOperations1();

protected abstract void RequiredOperation2();

// These are "hooks." Subclasses may override them, but it's not
// mandatory since the hooks already have default (but empty)
// implementation. Hooks provide additional extension points in some
// crucial places of the algorithm.
protected virtual void Hook1() { }

protected virtual void Hook2() { }
}

// Concrete classes have to implement all abstract operations of the base
// class. They can also override some operations with a default
// implementation.
class ConcreteClass1 : AbstractClass
{
protected override void RequiredOperations1()
{
Console.WriteLine("ConcreteClass1 says: Implemented Operation1");
}

protected override void RequiredOperation2()
{
Console.WriteLine("ConcreteClass1 says: Implemented Operation2");
}
}

// Usually, concrete classes override only a fraction of base class'
// operations.
class ConcreteClass2 : AbstractClass
{
protected override void RequiredOperations1()
{
Console.WriteLine("ConcreteClass2 says: Implemented Operation1");
}

protected override void RequiredOperation2()
{
Console.WriteLine("ConcreteClass2 says: Implemented Operation2");
}

protected override void Hook1()
{
Console.WriteLine("ConcreteClass2 says: Overridden Hook1");
}
}

class Client
{
// The client code calls the template method to execute the algorithm.
// Client code does not have to know the concrete class of an object it
// works with, as long as it works with objects through the interface of
// their base class.
public static void ClientCode(AbstractClass abstractClass)
{
// ...
abstractClass.TemplateMethod();
// ...
}
}

class Program
{
static void Main(string[] args)
{
Console.WriteLine("Same client code can work with different subclasses:");

Client.ClientCode(new ConcreteClass1());

Console.Write("\n");

Console.WriteLine("Same client code can work with different subclasses:");
Client.ClientCode(new ConcreteClass2());
}
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Same client code can work with different subclasses:
AbstractClass says: I am doing the bulk of the work
ConcreteClass1 says: Implemented Operation1
AbstractClass says: But I let subclasses override some operations
ConcreteClass1 says: Implemented Operation2
AbstractClass says: But I am doing the bulk of the work anyway

Same client code can work with different subclasses:
AbstractClass says: I am doing the bulk of the work
ConcreteClass2 says: Implemented Operation1
AbstractClass says: But I let subclasses override some operations
ConcreteClass2 says: Overridden Hook1
ConcreteClass2 says: Implemented Operation2
AbstractClass says: But I am doing the bulk of the work anyway

参考原文:模板方法设计模式