模板方法模式实现探讨
模板方法(Template Method)模式是 GOF 设计模式中最为常见几个模式之一。现在流行的很 多框架中(如 Spring,Struts 等),我们都可以看到模板方法模式的广泛应用。模板方法模式 主要应用于框架设计中,在日常的应用设计中也被经常使用。可是,我们在运用模板方法模 式来解决我们的需求而进行设计时,往往忽略了一些非常重要的细节。保证架构逻辑的正常 执行,不被子类破坏;怎么让子类扩展模板方法等。 1.模板方法设计模式的意图 通常我们会遇到这样的一个问题:我们知道一个算法所需的关键步骤,并确定了这些步骤的 执行顺序。但是某些步骤的具体实现是未知的,或者说某些步骤的实现与具体的环境相关。 模板方法模式把我们不知道具体实现的步骤封装成抽象方法,提供一个按正确顺序调用它们 的具体方法(这些具体方法统称为“模板方法”),这样构成一个抽象基类。子类通过继承这个抽 象基类去实现各个步骤的抽象方法,而工作流程却由父类控制。 考虑一个简单的订单处理需求:一个客户可以在一个订货单中订购多个货物(也称为订货单 项目),货物的销售价是根据货物的进货价进行计算的(不同货物有不同的售价计算方法)。有 些货物可以打折的,有些是不可以打折的。每一个客户都有一个信用额度,每张订单的总金 额不能超出该客户的信用额度。 根据上面的业务,我们可以知道处理一个订单所需要的步骤: 1.遍历订货单的订货单项目列表,累加所有货物的总价格(根据订货单项目计算出销售价); 2.根据客户号获得客户的信用额度; 3.把客户号,订单的总价格,及订单项目列表写入到数据库; 但是我们并不能确定怎么计算出货物的销售价,怎样根据客户号获得客户的信用额度及把订 单信息写入数据库这些方法的具体实现。 所以用一个抽象类 AbstractOrder 确定订单处理的逻辑,把不能确定的方法定义为抽象方法, 由子类去完成具体的实现。
public AbstractCacheContextTests extends TestCase...{
private static Map contextMap = new HashMap(); protected ConfigurableApplicationContext applicationContext;
protected abstract int getSpendingLimit(int customerId);
protected abstract int saveOrder(int customerId, int total, List orderItemList);
注意:模板方法模式中,迫使子类实现的抽象方法应该声明为 protected abstract。 6.模板方法模式与勾子方法(hookMethod) 上面讨论模板方法模式运用于一个业务对象。事实上,框架频繁使用模板方法模式,使得框 架实现对关键逻辑的集中控制。 思考这样的一个需求:我们需要为基本 Spring 的应用做一个测试类的基类,用于对类的方 法进行单元测试。我们知道 Spring 应用把需要关联的对象都定义在外部的 xml 文件中,也 称为 context。通常我们会把 context 分割成多个小的文件,以便于管理。在测试时我们需要 读取 context 文件,但是并不是每次都读取所有的文件。读取这些文件及实例化 context 中的 对象是很费时间的。所以我们想把它缓存起来,只要这个文件被读取过一次,我们就把它们 缓存起来。我们通过扩展 Junit 的 TestCase 类来定义一个测试基类。我们需要在这个基类中 实现缓存的逻辑,其它开发人员只需要实现读取配置文件的方法即可。它不用管是否具有缓 存功能。
在模板方法模式中有两个参与者进行协作。 抽象模板类:定义一个或多个抽象操作,由子类去实现。这些操作称为基本操作。 定义并实现一个具体操作,这个具体操作通过调用基本操作确定顶级逻辑。这个具体操作称 为模板方法。 具体类:实现抽象模板类所定义的抽象操作。 如上面的订单处理所示,AbstractOrder 就是抽象模板类,placeOrder 即是抽象模板方法。ge tOrderItemPrice,getSpendingLimit 和 saveOrder 三个抽象方法为基本操作。 具体子类能过需要去实现这三个抽象方法。不同的子类可能有着不同的实现方式。
Public class ConcreteOrder extends AbstractOrder...{ public int getOrderItemPrice(OrderItem orderItem)...{
//计算货物的售价 …… } public int getSpendingLimit(int customerId)...{ //读取客户的信用额度 ….. } public int saveOrder(int customerId, int total, List orderItem List)...{ //写入数据库 …… }
public abstract class AbstractOrder ...{
public Order placeOrder(int customerId , List orderItemList)...{
int total = 0;
for(int i = 0; i < orderItemList.size();i++)...{
protected boolean hasCachedContext(Object contextKey) ...{ return contextKeyToContextMap.containsKey(contextKey); } protected ConfigurableApplicationContext getContext(Object key) ...{ String keyString = contextKeyString(key); ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) contextKeyToContextMap.get(keyString); if (ctx == null) ...{ if (key instanceof String[]) ...{ ctx = loadContextLocations((String[]) key); } contextKeyToContextMap.put(keyString, ctx);
}
ConcreteOrder 为 AbstractOrder 的具体子类,ConcreteOrder 需要去完成具体的三个基本操作。 同时它也具有了父类一样的处理逻辑。把具体的实现延迟到了子类去实现,这就是模板方法 模式的关键。 3.模板方法模式与控制反转 “不要给我们打电话,我们会给你打电话”这是著名的好莱坞原则。在好莱坞,把简历递交给 演艺公司后就只有回家等待。由演艺公司对整个娱乐项的完全控制,演员只能被动式的接受 公司的差使,在需要的环节中,完成自己的演出。模板方法模式充分的体现了“好莱坞”原则。 由父类完全控制着子类的逻辑,这就是控制反转。子类可以实现父类的可变部份,却继承父 类的逻辑,不能改变业务逻辑。 4.模板方法模式与开闭原则 什么是“开闭原则”? 开闭原则是指一个软件实体应该对扩展开放,对修改关闭。也就是说软件实体必须是在不被 修改的情况下被扩展。模板方法模式意图是由抽象父类控制顶级逻辑,并把基本操作的实现 推迟到子类去实现,这是通过继承的手段来达到对象的复用,同时也遵守了开闭原则。 父类通过顶级逻辑,它通过定义并提供一个具体方法来实现,我们也称之为模板方法。通常 这个模板方法才是外部对象最关心的方法。在上面的订单处理例子中,public Order placeO rder(int customerId , List orderItemList) 这个方法才是外部对象最关心的方法。所以它必须 是 public 的,才能被外部对象所调用。 子类需要继承父类去扩展父类的基本方法,但是它也可以覆写父类的方法。如果子类去覆写 了父类的模板方法,从而改变了父类控制的顶级逻辑,这违反了“开闭原则”。我们在使用模 板方法模式时,应该总是保证子类有正确的逻辑。所以模板方法应该定义为 final 的。所以 AbstractOrder 类的模板方法 placeOrder 方法应该定义为 final。
那么把它定义为 protected.否则应该为 private。 显而易见,模板方法模式中的声明为 abstract 的基本操作都是需要迫使子类去实现的,它们 仅仅是为模板方法 placeOrder 服务的。它们不应该被 AbstractOrder 所公开,所以它们应该 p rotected。
protected abstract int getOrderItemPrice(OrderItem orderIte m);
OrderItem orderItem = (OrderItem)orderItemList.get(i);
total += getOrderItemPrice(orderItem) * orderItem.getQuantity(); } if(total > getSpendingLimit(customerId))...{ throw new BusinessException(“超出信用额度” + getSpendingLimit(custo merId)); } int orderId = saveOrder(customerId, total, orderItemList); return new OrderImpl(orderId,total); } public abstract int getOrderItemPrice(OrderItem orderItem); public abstract int getSpendingLimit(int customerId);
public abstract int saveOrder(int customerId, int total, List orderItemLis t);