设计二十三式之模板方法
意图
模板方法是一种行为设计模式,它在超类中定义算法的骨架,但让子类在不改变其结构的情况下覆盖算法的特定步骤。
02 问题
想象一下,您正在创建一个分析公司文档的数据挖掘应用程序。用户以各种格式(PDF、DOC、CSV)提供应用程序文档,并尝试以统一格式从这些文档中提取有意义的数据。
该应用程序的第一个版本只能使用 DOC 文件。在以下版本中,它能够支持 CSV 文件。一个月后,你“教”了它从 PDF 文件中提取数据。
在某些时候,您注意到所有三个类都有很多相似的代码。虽然所有类中处理各种数据格式的代码完全不同,但数据处理和分析的代码几乎相同。去掉代码重复,保持算法结构完好,不是很好吗?
还有另一个与使用这些类的客户端代码相关的问题。它有很多条件,可以根据处理对象的类别选择适当的操作过程。如果所有三个处理类都有一个公共接口或一个基类,那么您就可以消除客户端代码中的条件,并在调用处理对象上的方法时使用多态性。
03 解决方案
模板方法模式建议您将算法分解为一系列步骤,将这些步骤转化为方法,并将对这些方法的一系列调用放入单个模板方法中。这些步骤可能是abstract
,或者有一些默认实现。要使用该算法,客户端应该提供自己的子类,实现所有抽象步骤,并在需要时覆盖一些可选步骤(但不是模板方法本身)。
让我们看看这将如何在我们的数据挖掘应用程序中发挥作用。我们可以为所有三种解析算法创建一个基类。此类定义了一个模板方法,该方法由对各种文档处理步骤的一系列调用组成。
首先,我们可以声明所有步骤abstract
,强制子类为这些方法提供自己的实现。在我们的例子中,子类已经有了所有必要的实现,所以我们唯一需要做的就是调整方法的签名以匹配超类的方法。
现在,让我们看看我们可以做些什么来摆脱重复的代码。看起来打开/关闭文件和提取/解析数据的代码对于各种数据格式是不同的,所以没有必要接触这些方法。但是,其他步骤的实现,例如分析原始数据和编写报告,非常相似,因此可以将其拉到基类中,子类可以共享该代码。
如您所见,我们有两种类型的步骤:
每个子类都必须实现抽象步骤
可选步骤已经有一些默认实现,但如果需要仍然可以覆盖
还有另一种类型的步骤,称为hooks**。钩子是具有空主体的可选步骤。即使没有覆盖挂钩,模板方法也可以工作。通常,钩子放置在算法的关键步骤之前和之后,为子类提供算法的额外扩展点。
04 举个栗子
模板法方法可用于大规模住房建设。建造标准房屋的建筑计划可能包含几个扩展点,可以让潜在业主调整最终房屋的一些细节。
每一个建筑步骤,例如打地基、框架、砌墙、安装水管和水电布线等,都可以稍微改变一下,使最终的房子与其他房子有点不同。
05 结构实现
在此示例中,模板方法模式为简单的策略视频游戏中的各种人工智能分支提供了“骨架”。
游戏中的所有种族都有几乎相同类型的单位和建筑。因此,您可以为各种种族重用相同的 AI 结构,同时能够覆盖一些细节。使用这种方法,您可以覆盖兽人的 AI 使其更具攻击性,使人类更注重防御,并使怪物无法建造任何东西。向游戏中添加新种族需要创建一个新的 AI 子类并覆盖在基础 AI 类中声明的默认方法。
Show u the code:
// The abstract class defines a template method that contains a
// skeleton of some algorithm composed of calls, usually to
// abstract primitive operations. Concrete subclasses implement
// these operations, but leave the template method itself
// intact.
class GameAI is
// The template method defines the skeleton of an algorithm.
method turn() is
collectResources()
buildStructures()
buildUnits()
attack()
// Some of the steps may be implemented right in a base
// class.
method collectResources() is
foreach (s in this.builtStructures) do
s.collect()
// And some of them may be defined as abstract.
abstract method buildStructures()
abstract method buildUnits()
// A class can have several template methods.
method attack() is
enemy = closestEnemy()
if (enemy == null)
sendScouts(map.center)
else
sendWarriors(enemy.position)
abstract method sendScouts(position)
abstract method sendWarriors(position)
// Concrete classes have to implement all abstract operations of
// the base class but they must not override the template method
// itself.
class OrcsAI extends GameAI is
method buildStructures() is
if (there are some resources) then
// Build farms, then barracks, then stronghold.
method buildUnits() is
if (there are plenty of resources) then
if (there are no scouts)
// Build peon, add it to scouts group.
else
// Build grunt, add it to warriors group.
// ...
method sendScouts(position) is
if (scouts.length > 0) then
// Send scouts to position.
method sendWarriors(position) is
if (warriors.length > 5) then
// Send warriors to position.
// Subclasses can also override some operations with a default
// implementation.
class MonstersAI extends GameAI is
method collectResources() is
// Monsters don't collect resources.
method buildStructures() is
// Monsters don't build structures.
method buildUnits() is
// Monsters don't build units.
06 适用场景
当您想让客户端仅扩展算法的特定步骤而不是整个算法或其结构时,请使用模板方法模式。
模板方法允许您将单一算法转换为一系列单独的步骤,这些步骤可以很容易地被子类扩展,同时保持超类中定义的结构完整。
当您有几个类包含几乎相同的算法但有一些细微差别时,请使用该模式。因此,您可能需要在算法更改时修改所有类。
当你把这样的算法变成模板方法时,你也可以把实现类似的步骤拉到一个超类中,从而消除代码重复。子类之间变化的代码可以保留在子类中。
07 如何实施
分析目标算法,看看是否可以将其分解为步骤。考虑哪些步骤对所有子类都是通用的,哪些步骤总是唯一的。
创建抽象基类并声明模板方法和一组表示算法步骤的抽象方法。通过执行相应的步骤,在模板方法中概述算法的结构。考虑制作模板方法
final
以防止子类覆盖它。如果所有步骤最终都是抽象的,那也没关系。但是,某些步骤可能会受益于默认实现。子类不必实现这些方法。
考虑在算法的关键步骤之间添加挂钩。
对于算法的每个变体,创建一个新的具体子类。它必须实现所有抽象步骤,但也可能覆盖一些可选步骤。
08 优缺点
关注公众号可获取干货资料:《设计模式详解》《JAVA基础知识总结》 《java面试手册》等