Template Method模式类似于Adapter模式,原因在于两种模式都允许开发者简化和指定其他开发者程序代码完成设计的方式。在Adapter模式中,一个开发者也许指定设计所需的对象的接口。第二个开发者会创建一个对象,提供设计所期望的接口,但是使用具有其他接口的已有类的服务。在Template Method模式中,一个开发者也许提供一个通用算法,而第二个开发者提供该算法的关键步骤。
Aster公司的火药球填压机接受空的金属模具,然后将火药球压入模具里面。该机器有若干个料斗(hopper),各种化学品在料斗中被配制成药糊,随后,这些药糊被压入模具中。当机器关闭时,处理区中的模具将不再被处理,输入传送带上的所有模具都将经由处理区到达输出传送带,但不再处理这些模具。然后机器倒空当前剩余的药糊,并用水清洗处理区。填压机根据板载计算机发出的指令有条不紊地完成了上述工作。示意图如下:
图2 Aster火药球填压机提供输入输出传送带来移动火药球模具。
Ooninoz公司添加一个回收传送带,用于回收被倒空的药糊
Aster公司的火药球填压机提供一个抽象类,在Oozinoz系统中我们必须为其提供一个子类才能使填压机运转
Aster火药球填压机具有一定的智能,并能能够独立工作。其开发人员已经考虑到填压机可能用于智能化的工厂里,并且可能需要与其他设备通信。例如,如果正在处理的模具尚未处理完毕,那么shutDown()方法将会通知工厂:
markMoldIncomplete()方法和AsterStarPress类都是抽象的。在Oozinoz公司的系统中,我们需要为其创建一个子类,并实现其中所需的方法,然后将该代码下载到火药球填压机的板载计算机上。在markMoldIncomplete()方法的实现中,我们将把未完成的模具信息传送给MaterialManager单例,从而实现对原料状态的跟踪。
突破题:请实现OzAsterStarPress类的markMoldIncomplete()方法。
答:在markMoldIncomplete()方法体内,一个没有处理完毕的模具信息被传给了原材料管理器,具体代码如下:
public class OzAsterStarPress extends AsterStarPress { public void markMoldIncomplete(int id){ MaterialManager.getManager().setMoldIncomplete(id); } }
Aster火药球填压机的开发人员清楚地知道焰火工厂的生产流程,因此他们已经在填压机上提供了与其他工厂设备进行通信的支持。虽然如此,还有可能存在某些地方是Aster的开发人员所没有预见到的,这个时候就需要我们自己来实现填压机与其他设备的通信。
3. 模式钩子:
钩子就是一个方法调用。开发人员可以将它放到自己代码中的特定位置,这样其他开发人员便可以在该位置插入他们自己的代码。当我们希望适配其他开发人员的代码的时候,或者当我们需要在其他人的代码中插入自己的控制的时候,就可以使用钩子。其他开发人员如果愿意帮忙的话,通常会允许我们在他们代码的适当地方插入一个方法调用。开发人员通常会为自己代码中的钩子提供一个存根实现,这样,不需要使用钩子的其他用户就不必重写这个钩子方法。
下面再考虑前面Aster公司压缩机的例子。当机器关闭时,Aster火药球填压机将倒空化学品药糊,并用水清洗处理区。填压机必须倒空该药糊,以防止药糊干燥后堵塞机器。在火药球填压机倒空药糊后,我们可以安排一个机器人将药糊转移到另外一个传送带上。现在的问题是,我们期望在shutdown()方法的两个语句间获得对系统的控制权:
dischargePaste(); flush();
我们可能会在代码中调用收集药糊的方法,从而重写dischargePaste():
public void dischargePaste() { super.dischargePaste(); getFactory().collectPaste(); }
该方法在清空药糊后有效地插入一个步骤。所添加的步骤使用一个Factory单例收集火药球填压机倒空的药糊。当执行shutdown()方法时,工厂机器人会在shutdown()方法用水清洗处理区之前搜集废弃的药糊。不幸的是,dischargePaste()方法代码非常危险,因为药糊的收集工作是在排空药糊的方法中附带完成的。Aster的开发人员一定不会知道你会有这种方式来定义dischargePaste()方法。如果他们修改代码,在你不想收集药糊的时候倒空药糊,这将是一个错误。
开发人员通常都是通过努力的修改代码来解决问题。但是,这里的难点在于开发人员要通过与其他开发人员沟通来解决这个问题。
自我突破题:请通知Aster的开发人员,要求他们做一些改变,让我们能够在机器冲洗处理区之前安全地悼收集被倒空的药糊。
答:可能会这样陈述请求:我希望你能够在shutDown()方法中增加一个调用,该调用在机器排空药糊之后和机器开始注水之前执行。如果将这个方法称为collectPaste(),那么我们可以在该方法中保存药糊,从而使得药糊可以被Oozinoz公司重复利用。
开发人员很可能会与你协商从而确定这个方法的名称。名称并不重要,重要是通过在一个模板方法中请求一个钩子,可以解决现有代码存在的问题,并使我们的代码更加健壮。
在Template Method模式中,我们可以利用子类提供的步骤来完成某个算法,这个步骤可以是一个可选的步骤,并且能够根据其他开发人员的需要挂到子类的代码中。尽管该模式的目的是使用一个单独的类来定义算法的一部分,但是,当我们需要对出现在多个方法中的算法进行重构的时候,也可以使用Template Method模式。
4.重构为Template Method模式:
在应用Template Method模式的时候,层次结构中的类通常会使用我们或者其他人在一个抽象的超类中提供的算法框架。当我们在多个方法中实现类似的算法时,也可以重构为Template Method模式(重构是使用更好的设计思路重新实现等价功能的过程)。下面我们再看看 工厂方法模式 中的Machine和MachinePlanner层次结构。如下图4所示,Machine类将createPlanner()方法作为Factory Method模式,该方法返回了MachinePlanner层次结构中的一个适当的子类。
一个Machine对象能够为自己创建一个适当的MachinePlanenr实例
当要求创建一个规划器时,Machine类的两个子类会实例化MachinePlanner类层次结构中指定的子类。这些类---ShellAssembler和StarePress---存在相同的问题,因为它们希望只在需要时创建MachinePlanner。
查看这些类的代码,我们注意到子类使用了类似于滞后初始化规划器这样的技术。比如ShellAssembler类包含getPlanner()方法,滞后实例化planner成员:
public ShellPlanner getPlanner() { if(planner == null) planner = new ShellPlanner(this); return planner; }
StarPress类的getPlanner()方法也对planner属性进行滞后初始化:
public StarPressPlanner getPlanner() { if(planner == null) planner = new StarPressPlanner(this); return planner; }
Machine类的其他子类也使用了类似的技术,仅在第一次使用时才创建规划器(planner)对象。这样就需要重构代码,需要清理和缩减维护的代码量。假设你决定给Machine类提供一个类型为MachinePlanner的planner属性,需要从子类中删除这个属性,并删除已有的getPlanner()方法。
突破题:请写出Machine类中getPlanner()方法的实现代码:
答:Machine类的getPlanner()方法充分利用了抽象的createPlanner()方法。具体代码如下:
public MachinePlanner getPlanner() { if(planner == null) planner = createPlanner(); return planner; }
这部分代码要求你往Machine类中增加planner字段。在往Machine类中加入这个属性和getPlanner()方法之后,我们可以删掉Machine子类中的这个属性和这个方法。
这种重构可以创建Template Method模式的一个实例。getPlanner()方法对规划器变量进行了滞后初始化,而初始化过程使用子类提供的createPlanner()方法。
我们可以把自己的代码重构为Template Method模式的实例,具体的做法是:将类似方法的框架抽象出来,并将方法框架上移到超类中,而子类只需提供在算法的实现过程中不同的那些步骤。
5.小结:
Template Method模式的目的在于在一个方法中定义一个算法,并对算法的某些步骤进行抽象,这样,我们可以将这些步骤的具体实现从这个方法中提出,并在其外部方法中定义这些步骤,或者是用一个接口定义这些步骤,留待其他类来提供这些步骤的实现。
Template Method模式通常可以作为开发人员之间的某种约束。一个开发人员提供算法的框架,另一个开发人员则提供算法某些步骤的具体实现。这也许是需要算法实现的步骤,或者是算法的开发人员在算法中某一位置设置的钩子。
Template Method模式并不要求我们必须在定义子类前编写模板方法。我们可能在已有的类层次结构中发现相似的方法。在这种情况下,我们可以抽象出算法的框架,并把它上移到超类中,从而应用Template Method模式来简化和组织代码。