定义:定义一个操作中算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法中的某些特定步骤。
类型:类行为型模式
类图:
AbstractClass:抽象类。用来定义算法骨架和原语操作,具体的子类可以通过重定义这些原语操作来实现一个算法的各个步骤。在这个类里面,还可以提供算法中通用的实现。
ConcreteClass:具体实现类。用来实现算法骨架中的某些步骤,完成与特定子类相关的功能。
模板方法模式示例代码
(1) 先来看看AbstractClass的写法,示例代码如下:
/** * 定义模板方法、原语操作等的抽象类 * @author FX_SKY * */ public abstract class AbstractClass { /** * 原语操作1,所谓原语操作就是抽象的操作,必须要由子类提供实现 */ public abstract void doPrimitiveOperation1(); /** * 原语操作2 */ public abstract void doPrimitiveOperation2(); /** * 模板方法,定义算法骨架 */ public final void templateMethod(){ doPrimitiveOperation1(); doPrimitiveOperation2(); } }
/** * 具体实现类,实现原语操作 * @author FX_SKY * */ public class ConcreteClass extends AbstractClass { @Override public void doPrimitiveOperation1() { // 具体的实现 } @Override public void doPrimitiveOperation2() { // 具体的实现 } }
场景问题 — 登陆控制
几乎所有的应用系统,都需要系统登陆控制的功能,有些系统甚至有多个登陆控制的功能,比如,普通用户可以登陆前台,进行相应的业务操作;而工作人员可以登陆后台,进行相应的系统管理或业务处理。
现在有这么一个基于Web的企业级应用系统,需要实现这两种登陆控制,直接使用不同的登陆界面来区分它们,把基本的功能需求描述如下:
普通用户登陆前台的登陆控制的功能。
再看看工作人员登陆后台的登陆控制功能。
(1) 封装登陆控制所需要的数据模型,示例代码如下:
/** * 封装进行登陆控制所需要的数据 * @author FX_SKY * */ public class LoginModel { /** * 登陆人员的编号id,通用的,可能是用户编号,也可能是工作人员编号 */ private String loginId; /** * 登陆的密码 */ private String pwd; public String getLoginId() { return loginId; } public void setLoginId(String loginId) { this.loginId = loginId; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } }
(2) 接下来定义公共的登陆控制算法骨架,示例代码如下:
/** * 登陆控制的模板 * * @author FX_SKY * */ public abstract class LoginTemplate { /** * 判断登陆数据是否正确,也就是是否能登陆成功 * @param lm * @return */ public final boolean login(LoginModel lm) { //1 根据登陆人员编号去获取相应的数据 LoginModel dbLm = this.findLoginUser(lm.getLoginId()); if(dbLm!=null){ //2 对密码进行加密 String encryptPwd = this.encryptPwd(lm.getPwd()); //把加密后的密码设置回到登陆数据模型中 lm.setPwd(encryptPwd); //3 判断是否匹配 return this.match(lm, dbLm); } return false; } /** * 根据登陆编号来超重和获取存储中相应的数据 * @param loginId * @return */ public abstract LoginModel findLoginUser(String loginId); /** * 对密码数据进行加密 * @param pwd * @return */ public String encryptPwd(String pwd) { return pwd; } /** * 判断用户填写的登陆数据和存储中对应的数据算法匹配得上 * @param lm * @param dbLm * @return */ public boolean match(LoginModel lm, LoginModel dbLm) { if (lm.getLoginId().equals(dbLm.getLoginId()) && lm.getPwd().equals(dbLm.getPwd())) { return true; } return false; } }
(3) 实现新的普通用户登陆控制的逻辑处理,示例代码如下:
/** * 普通用户登陆控制的逻辑处理 * @author FX_SKY * */ public class NormalLogin extends LoginTemplate { @Override public LoginModel findLoginUser(String loginId) { //这里省略具体的处理,仅做示意,返回一个有默认数据的对象 LoginModel lm = new LoginModel(); lm.setLoginId(loginId); lm.setPwd("testpwd"); return lm; } }
(4) 实现新的工作人员的登陆控制的逻辑处理,示例代码如下:
/** * 管理人员登陆控制的逻辑处理 * @author FX_SKY * */ public class WorkerLogin extends LoginTemplate { @Override public LoginModel findLoginUser(String loginId) { //这里省略具体的处理,仅做示意,返回一个有默认数据的对象 LoginModel lm = new LoginModel(); lm.setLoginId(loginId); lm.setPwd("testpwd"); return lm; } @Override public String encryptPwd(String pwd) { // 覆盖父类的方法,提供真正的加密实现 System.out.println("使用MD5进行密码加密"); return pwd; } }
(5) 客户端测试,示例代码如下:
public class Client { /** * @param args */ public static void main(String[] args) { // 准备登陆人的信息 LoginModel lm = new LoginModel(); lm.setLoginId("admin"); lm.setPwd("workerpwd"); //准备用来进行判断的对象 LoginTemplate lt = new WorkerLogin(); LoginTemplate lt2 = new NormalLogin(); //进行登陆测试 boolean flag = lt.login(lm); System.out.println("可以登陆管理工作平台="+flag); //进行登陆测试 boolean flag2 = lt2.login(lm); System.out.println("可以进行普通人员登陆="+flag2); } }
认识模板方法模式
1、模板方法模式的功能
模板方法模式的功能在于固定算法骨架,而让具体算法实现可扩展。
这在实际应用中非常广泛,尤其是设计框架级功能的时候非常有用。框架定义好了算法的步骤,在合适的点让开发人员进行扩展,实现具体的算法。比如在DAO实现中设计通用的增删改查功能,这个在后面会给大家示例。
模板方法模式还额外提供了一个好处,就是可以控制子类的扩展。因为在父类中定义好了算法的步骤,只是在某几个固定的点才会调用被子类实现的方法,因此也就只允许在这几个点来扩展功能。这些可以被子类覆盖以扩展功能的方法通常被称为“钩子”方法,在后面也好给大家示例。
2、为何不是接口
有的朋友可能会问一个问题,不是说在Java中应该尽量面向接口编程吗,为何模板方法的模板采用的是抽象方法呢?
要回答这个问题,首先要搞清楚抽象类和接口的关系:
因此,虽然Java编程中提倡大家“面向接口编程”,并不是说就不再使用抽象类了。那么什么时候使用抽象类呢?
通常在“既要约束子类的行为,又要为子类提供公共功能”的时候使用抽象类。
3、变与不变
程序设计的一个很重要的思考点就是“变与不变”,也就是分析程序中哪些功能是可变的,哪些功能是不变的,然后把不变的部分抽象出来,进行公共的实现,把变化的部分分离出去,用接口来封装隔离,或者是用抽象类来约束子类行为。
模板方法很好的体现了这一点。模板类实现的就是不变的方法和算法的骨架,而需要变化的地方,都通过抽象方法,把具体的实现延迟到子类中了,而且还通过父类的定义来约束了子类的行为,从而使系统能有更好的复用性和扩展性。
4、好莱坞法则
什么是好莱坞法则呢?简单点说,就是“不要找我们,我们会联系你”。
模板方法模式很好的体现了这一点,作为父类的模板会在需要的时候,调用子类相应的方法,也就是由父类来找子类,而不是让子类来找父类。
这其实也是一种反向的控制结构。按照通常的思路,是子类找父类才对,也就是应该是子类来调用父类的方法,因为父类根本就不知道子类,而子类是知道父类的,但是在模板方法模式里面,是父类来找子类,所以是一种反向的控制结构。
那么Java里面能实现这样功能的理论依据在哪里呢?
理论依据就在于Java 的动态绑定采用的是“后期绑定”技术,对于出现子类覆盖父类方法的情况,在编译时是看数据类型,运行时则看实际的对象类型(new 操作符后跟的构造方法是哪个类的),一句话:new 谁就用谁的方法。
模板方法的写法
在实现模板的时候,到底哪些方法实现在模板上呢?模板能不能全部实现了,也就是模板不提供抽象方法呢?当然,就是没有抽象方法,模板一样可以定义为抽象类。
通常在模板里面包含以下操作类型。
模板方法:就是定义算法骨架的方法。
具体的操作:在模板方法中直接实现某些步骤的方法。通常这些步骤的实现算法是固定的,而且是不怎么变化的,因此可以将其当做公共功能实现在模板方法中。如果不需为子类提供访问这些方法的话,还可以是private的。这样一来,子类的实现就相对简单些。如果是子类需要访问,可以把这些方法定义为protected final 的,因为通常情况下,这些实现不能够被子类覆盖和改变了。
具体的AbstractClass操作:在模板中实现某些公共功能,可以提供给子类使用,一般不是具体的算法步骤的实现,而是一些辅助的公共功能。
原语操作:就是在模板中定义的抽象操作,通常是模板方法需要调用的操作,是必须的操作,而且在父类中还没有办法确定下来如何实现,需要子类来真正实现的方法。
钩子操作:在模板中定义,并提供默认实现的操作。这些方法通常被视为可扩展的点,但不是必须的,子类可以有选择的覆盖这些方法,以提供新的实现来扩展功能。比如模板方法中定义了5步操作,但是根据需要,某种具体的实现只需要其中的1、2、3几个步骤,因此它就只需要覆盖实现1、2、3这几个步骤对应的方法。那么4和5步骤对应的方法怎么办呢,由于有默认实现,那就不用管了。也就是说钩子操作是可以被扩展的点,但不是必须的。
Factory Method:在模板方法中,如果需要得到某些对象实例的话,可以考虑通过工厂方法模式来获取,把具体的构建对象的实现延迟到子类中去。
总结起来,一个较为完整的模板定义示例,其示例代码如下:
/** * 一个较为完整的模板定义示例 * @author FX_SKY * */ public abstract class AbstractTemplate { /** * 模板方法,定义算法骨架 */ public final void templateMethod(){ //第一步 this.operation1(); //第一步 this.operation2(); //第三步 this.doPrimitiveOperation1(); //第四步 this.doPrimitiveOperation2(); //第五步 this.hookOperation1(); } /** * 具体操作1:,算法中的步骤,固定实现,而且子类不需要访问 */ private void operation1() { // 在这里具体的实现 } /** * 具体操作2:,算法中的步骤,固定实现,子类可能需要访问 * 当然也可以定义为public的,不可以被覆盖,因此是final的 */ protected final void operation2() { // 在这里具体的实现 } /** * 具体的AbstractClass操作,子类的公共功能 * 但通常不是具体的算法步骤 */ protected final void commonOperation() { // 在这里具体的实现 } /** * 原语操作1,算法中的必要步骤,父类无法确定如何真正实现,需要子类来实现 */ protected abstract void doPrimitiveOperation1(); /** * 原语操作2,算法中的必要步骤,父类无法确定如何真正实现,需要子类来实现 */ protected abstract void doPrimitiveOperation2(); /** * 钩子操作,算法中的步骤,不一定需要,提供默认实现 * 由子类选择并具体实现 */ protected void hookOperation1() { // 在这里具体的实现 } /** * 工厂方法,创建某个对象,这里用Object代替了,在算法实现中可能需要 * @return 创建某个算法实现需要的对象 */ protected abstract Object createOneObject(); }
模板方法模式的优缺点
模板方法模式是一种实现代码复用的很好的手段,通过把子类的公共功能提炼和抽取,把公共部分放到模板中去实现。
模板方法模式最基本的功能就是通过模板的制定,把算法骨架完全固定下来,事实上模板和子类是非常耦合的,如果要对模板 中的算法骨架进行变更,可能就会要求所有相关的子类进行相应的编号,所有抽取算法骨架的时候要特别小心,尽量确保是不 会变化的部分才放到模板中。
思考模板方法模式
1、模板方法模式的本质
模板方法模式的本质:固定算法骨架。
模板方法模式主要是通过制定模板,把算法步骤固定下来,至于谁来实现,模板可以自己实现,也可以由子类去实现,还可以通过回调机制让其它类来实现。
通过固定算法骨架来约束子类的行为,并在特定的扩展点来让子类进行功能扩展,从而让程序既有很好的复用性,又有较好的扩展性。
2、对设计原则的体现
模板方法很好的体现了开闭原则和里氏替换原则。
首先从设计上分离变与不变,然后把不变的部分抽取出来,定义到父类中,比如算法骨架,一些公共的、固定的实现等。这些不变的部分被封闭起来,尽量不去修改它们,要想扩展新的功能,那就使用子类来扩展,通过子类来实现可变化的步骤,对于这种新增功能的做法是开放。
其次,能够实现统一的算法骨架,通过切换不同的具体实现来切换不同的功能,一个根本原因就是里氏替换原则,遵循这个原则,保证所有的子类实现的是同一个算法模板,并能在使用模板的地方,根据需要切换不同的具体实现。