在微软的WebCast上,李建中老师有个设计模式的系列讲座,其中在讲到模板方法(Template Method)曾说:如果你只想学习一种设计模式就学习模板方法吧。由此可见它使用的广泛性。 今天咱们就来一起学习模板方法模式。
那么,什么是模板方法模式呢?在解决这个问题前,咱们先来看看模板。提到模板,相信大家马上能够想到一些东西,如ppt的模板,报表导出的excel模板,简历的模板等等,呵呵,使用它们的好处当然是显而易见的:它们可以给我们提供特定的结构和样式,我们就只需关心填充数据内容。将模板的思想发散到编程上,就是我们今天的主题了。
模板方法Gof的定义是:在一个方法里定义算法的骨架,将一些步骤延迟到其子类。如下图:
AbstractClass主要是定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。这有点像C语言中的一个“钩子(hook)”;同时将公用的代码移植到TemplateMethod中,实现的代码的公用。
我们来看一个模板方法的应用
假设教育部规定了高校新生报到的流程(这好比写好了算法的骨架),流程为凭录取通知书到教务处报到->缴费->本院系报到(获取自己的专业班级信息)->教材科领取教材。
public class 高校
{
//这就是一个模板方法,定义了算法的骨架
public final void 报到()
{
教务处报到();
缴费();
本院系报到();
教材科发教材();
}
protected abstract void 教务处报到();
protected abstract void 缴费();
protected abstract 专业等信息本院系报到();
protected abstract 教材教材科发教材();
}
但是各个具体的院校因为自己的情况对这个步骤作进一步补充:清华大学(该校信息技术及其发达,处处透露出信息化气息)。清华大学的学生领取的通知书是一张磁卡,学生拿着磁卡到教务处报到只需要去刷卡即可,该卡还具有银行支付功能,学生只需要向该卡转入学费就可以完成缴费,但是必须是先到教务处报到后该卡才生效。学生缴费以后由电子系统自动到学生院系报到并通过手机短信方式将学生专业班级信息发送给学生,完成后由学校的物流配送系统将学生教材送到学生寝室。
public class 清华大学 extends 高校
{
protected void 教务处报到()
{
//刷卡
}
protected void 缴费()
{
//银行卡转账
}
protected 专业等信息本院系报到()
{
//系统自动将学生信息转入到院系信息系统
//系统将学生专业班级信息发送到学生手机
return 手机短信方式的专业等信息
}
protected 教材教材科发教材()
{
//学校物流配送系统送教材
return 教材
}
}
北京大学(该校认为作为有深厚文化底蕴的高校,应该注重人文气息,不能用冷冰冰的电子设备代替人工,所以该校报到过程全部人工化)该校通知书为普通纸片,到教务处报到需要学生签字处长签字,然后到财务处缴纳学费,然后到院系见书记一面,给了你一张有专业班级等信息的卡片,最后拿个袋子到教材科装教材回寝室了。
public class 北京大学 extends 高校
{
protected void 教务处报到()
{
//学生签字
//教务处处长签字
//报到完毕
}
protected void 缴费()
{
//到财务处缴纳现金
}
protected 专业等信息到本院系报到()
{
//见到院书记
return 纸片方式的专业等信息
}
protected 教材教材科发教材()
{
//自备袋子到教材科
return 教材
}
}
以上是两个高校针对教育部颁发的新生报到算法作出自己具体的实现。那么全国有这么多高校又有多少种实现呢?让教育部分别给每所高校规定新生报到算法,这样是不现实的,如果教育部给所有高校规定同一个样子的报到流程就失去了灵活性,毕竟要考虑实际嘛。
通过上面的描述是不是觉得模板方法好像是:上有政策,下有对策?对,模板方法就是上面的这个政策,各个下级有自己对政策中具体步骤的实现。模板方法就像我们的宪法一样,对其他所有法律指定了一些大的框架,要制订新的法律必须依照这个框架。
看过这个例子后,大家可以思考一下,在java中,我们见过这种设计模式的身影吗?哦,搜索一下还真是不少!主要是在一些框架譬如Junit,Struts,Spring等的设计中大量采用。
(1) JUnit中的TestCase以及它的子类就是一个模板方法模式的例子。在TestCase这个抽象类中将整个测试的流程设置好,比如先执行setup方法初始化测试资源,再运行测试方法,然后再tearDown来释放测试资源。但是我们将在setup、tearDown里面作些什么?谁知道呢?!因此,这些步骤的具体实现都会延迟到子类中去,也就是我们实现的测试类中。
(2) Struts框架控制器的核心类RequestProcess里面的process方法也是采用了经典的模板方法模式,里面定义好了流程的步骤,而步骤里面的很多环节,我们都是可以重写的。
(3)Spring用另一种方式实现了Template Method模式,我们来详细解读一下。Spring对Hibernate的调用提供了HibernateDaoSupport的支持,在该类中,有了一个HibernateTemplate类来调用Hiebarnate接口,通过另外一种方式使用Template Method模式,避免了开发者代码中出现诸如Session管理、事务控制等重复代码。
现在我们看看Spring是如何做Template的,打开spring源代码发现在HibernateTemplate中包含execute(HibernateCallback action);该方法扮演的角色就是类似与上图中的AbstractClass类中的TemplateMothod.而该方法有只有一个唯一参数就是接口HibernateCallback所引用的对象action,在改接口抽象出了变化的数据库操作,:
public interface HibernateCallback {
Object doInHibernate(Session session) throws HibernateException, SQLException;
}
就像我们在前面形容的一样HibernateCallback扮演一个“钩子(hook)“的角色,这样用接口和不同的接口实现类替代了Template Method模式中的继承关系同时也将模板方法execute(HibernateCallback action)独立出来。
看看execute的实现:
public Object execute(HibernateCallback action, boolean exposeNativeSession) throws DataAccessException {
Session session = getSession();
boolean existingTransaction = SessionFactoryUtils.isSessionTransactional(session, getSessionFactory());
if (existingTransaction) {
logger.debug("Found thread-bound Session for HibernateTemplate");
}
FlushMode previousFlushMode = null;
try {
previousFlushMode = applyFlushMode(session, existingTransaction);
Session sessionToExpose = (exposeNativeSession ? session : createSessionProxy(session));
Object result = action.doInHibernate(sessionToExpose);
flushIfNecessary(session, existingTransaction);
return result;
}
catch (HibernateException ex) {
throw convertHibernateAccessException(ex);
}
catch (SQLException ex) {
throw convertJdbcAccessException(ex);
}
catch (RuntimeException ex) {
// Callback code threw application exception...
throw ex;
}
finally {
if (existingTransaction) {
logger.debug("Not closing pre-bound Hibernate Session after HibernateTemplate");
if (previousFlushMode != null) {
session.setFlushMode(previousFlushMode);
}
}
else {
SessionFactoryUtils.releaseSession(session, getSessionFactory());
}
}
}
我们可以看到,在excute方法中做了对Session的管理和事务的控制,而代码:
Object result = action.doInHibernate(sessionToExpose);
则执行实际的数据库操作。
我们看看HibernateTemplate对hibernate的一个get方法的封装:
public Object get(final Class entityClass, final Serializable id, final LockMode lockMode)
throws DataAccessException {
return execute(new HibernateCallback() {
public Object doInHibernate(Session session) throws HibernateException {
if (lockMode != null) {
return session.get(entityClass, id, lockMode);
}
else {
return session.get(entityClass, id);
}
}
}, true);
}
在该段代码中用匿名类声明了一个接口实现,并将改对象作为参数传给excute方法。如此,便实现了Template Method设计模式。
从整体来看,我觉得有几点可以值得关注:
1.用接口代替继承来抽象出hibernate的不同数据库操作,达到一种更加轻量级的效果
2.将对变动的与不变的相互分离,在本例中不变的是Session和事务管理,变动的是不同的数据库操作
3.通过封装隐藏Template Method模式,对用户的使用是透明的
这样就是一种更优雅的方式实现了TemplateMethod模式。
下面我们来对末班方法模式做一个总结:
1.模板方法设计模式的意图
通常我们会遇到这样的一个问题:我们知道一个算法所需的关键步骤,并确定了这些步骤的执行顺序。但是某些步骤的具体实现是未知的,或者说某些步骤的实现与具体的环境相关。
模板方法模式把我们不知道具体实现的步骤封装成抽象方法,提供一个按正确顺序调用它们的具体方法(这些具体方法统称为“模板方法”),这样构成一个抽象基类。子类通过继承这个抽象基类去实现各个步骤的抽象方法,而工作流程却由父类控制。
2.模板方法模式定义及结构
模板方法模式属于行为模式的一种(GOF95)。准备一个抽象类,定义一个操作中的算法的骨架,将一些步骤声明为抽象方法迫使子类去实现。不同的子类可以以不同的方式实现这些抽象方法。
3.模板方法模式与控制反转
“不要给我们打电话,我们会给你打电话”这是著名的好莱坞原则。在好莱坞,把简历递交给演艺公司后就只有回家等待。由演艺公司对整个娱乐项的完全控制,演员只能被动式的接受公司的差使,在需要的环节中,完成自己的演出。模板方法模式充分的体现了“好莱坞”原则。由父类完全控制着子类的逻辑,这就是控制反转。子类可以实现父类的可变部份,却继承父类的逻辑,不能改变业务逻辑。
注意:模板方法模式中,抽象类的模板方法应该声明为final的。因为子类不能覆写一个被定义为final的方法。从而保证了子类的逻辑永远由父类所控制。
4.模板方法模式与开闭原则
什么是“开闭原则”?
开闭原则是指一个软件实体应该对扩展开放,对修改关闭。也就是说软件实体必须是在不被修改的情况下被扩展。模板方法模式意图是由抽象父类控制顶级逻辑,并把基本操作的实现推迟到子类去实现,这是通过继承的手段来达到对象的复用,同时也遵守了开闭原则。
父类通过顶级逻辑,它通过定义并提供一个具体方法来实现,我们也称之为模板方法。通常这个模板方法才是外部对象最关心的方法。所以它必须是public的,才能被外部对象所调用。同时,因为子类不能覆写一个被定义为final的方法。从而保证了子类的逻辑永远由父类所控制。
5.模板方法模式与对象的封装性
面向对象的三大特性:继承,封装,多态。
对象有内部状态和外部的行为。封装是为了信息隐藏,通过封装来维护对象内部数据的完整性。使得外部对象不能够直接访问一个对象的内部状态,而必须通过恰当的方法才能访问。
在Java中,采用给对象属性和方法赋予指定的修改符(public、protected、private)来达到封装的目的,使得数据不被外部对象恶意的访问及方法不被错误调用导造成破坏对象的封装性。
注意:模板方法模式中,迫使子类实现的抽象方法应该声明为protected abstract。
6. 模板方法和其他模式的联系
各个模式之间都有联系,模板方法也不例外,她并不是孤立存在的。模板中的那些虚方法实际上都是使用工厂方法设计模式,将父类的执行逻辑延迟到子类。有的时候模板方法里定义算法的步骤会用到策略模式,因为有的时候这个算法不止一种,比如上面的教育部规定新生报到流程这个算法,有可能教育部规定了三四种,那么我们就可以用策略模式封装这几套算法。
7.模板方法的变形
学习设计模式最忌讳的就是生搬硬套,我们上面给出的Spring的HibernateTemplate源码分析就是告诉大家设计模式一定要领会精髓,活学活用。