`

模板模式3

 
阅读更多

16.3  模式讲解

16.3.1  认识模板方法模式

(1)模式的功能

    模板方法的功能在于固定算法骨架,而让具体算法实现可扩展。

    这在实际应用中非常广泛,尤其是在设计框架级功能的时候非常有用。框架定义好了算法的步骤,在合适的点让开发人员进行扩展,实现具体的算法。比如在DAO实现中,设计通用的增删改查功能,这个后面会给大家示例。

    模板方法还额外提供了一个好处,就是可以控制子类的扩展。因为在父类里面定义好了算法的步骤,只是在某几个固定的点才会调用到被子类实现的方法,因此也就只允许在这几个点来扩展功能,这些个可以被子类覆盖以扩展功能的方法通常被称为“钩子”方法,后面也会给大家示例。

(2)为何不是接口

    有的朋友可能会问一个问题,不是说在Java中应该尽量面向接口编程吗,为何模板方法的模板是采用的抽象方法呢?

    要回答这个问题,要首先搞清楚抽象类和接口的关系:

  • 接口是一种特殊的抽象类,所有接口中的属性自动是常量,也就是public final static的,而所有接口中的方法必须是抽象的
  • 抽象类,简单点说是用abstract修饰的类。这里要特别注意的是抽象类和抽象方法的关系,记住两句话:抽象类不一定包含抽象方法;有抽象方法的类一定是抽象类
  • 抽象类和接口相比较,最大的特点就在于抽象类里面是可以有具体的实现方法的,而接口中所有的方法都是没有具体的实现的。

因此,虽然Java编程中倡导大家“面向接口编程”,并不是说就不再使用抽象类了,那么什么时候使用抽象类呢?

通常在“既要约束子类的行为,又要为子类提供公共功能”的时候使用抽象类。

按照这个原则来思考模板方法模式的实现,模板方法模式需要固定定义算法的骨架,这个骨架应该只有一份,算是一个公共的行为,但是里面具体的步骤的实现又可能是各不相同的,恰好符合选择抽象类的原则。

把模板实现成为抽象类,为所有的子类提供了公共的功能,就是定义了具体的算法骨架;同时在模板里面把需要由子类扩展的具体步骤的算法定义成为抽象方法,要求子类去实现这些方法,这就约束了子类的行为。

因此综合考虑,用抽象类来实现模板是一个很好的选择。

(3)变与不变

    程序设计的一个很重要的思考点就是“变与不变”,也就是分析程序中哪些功能是可变的,哪些功能是不变的,然后把不变的部分抽象出来,进行公共的实现,把变化的部分分离出去,用接口来封装隔离,或者是用抽象类来约束子类行为。

    模板方法模式很好的体现了这一点。模板类实现的就是不变的方法和算法的骨架,而需要变化的地方,都通过抽象方法,把具体实现延迟到子类去了,而且还通过父类的定义来约束了子类的行为,从而使系统能有更好的复用性和扩展性。

(4)好莱坞法则

    什么是好莱坞法则呢?简单点说,就是“不要找我们,我们会联系你”。

    模板方法模式很好的体现了这一点,做为父类的模板会在需要的时候,调用子类相应的方法,也就是由父类来找子类,而不是让子类来找父类。

    这其实也是一种反向的控制结构,按照通常的思路,是子类找父类才对,也就是应该是子类来调用父类的方法,因为父类根本就不知道子类,而子类是知道父类的,但是在模板方法模式里面,是父类来找子类,所以是一种反向的控制结构。

    那么,在Java里面能实现这样功能的理论依据在哪里呢?

    理论依据就在于Java的动态绑定采用的是“后期绑定”技术,对于出现子类覆盖父类方法的情况,在编译时是看数据类型,运行时看实际的对象类型(new操作符后跟的构造方法是哪个类的),一句话:new谁就调用谁的方法

    因此在使用模板方法模式的时候,虽然用的数据类型是模板类型,但是在创建类实例的时候是创建的具体的子类的实例,因此调用的时候,会被动态绑定到子类的方法上去,从而实现反向控制。其实在写父类的时候,它调用的方法是父类自己的抽象方法,只是在运行的时候被动态绑定到了子类的方法上。

(5)扩展登录控制

    在使用模板方法模式实现过后,如果想要扩展新的功能,有如下几种情况:

一种情况是只需要提供新的子类实现就可以了,比如想要切换不同的加密算法,现在是使用的MD5,想要实现使用3DES的加密算法,那就新做一个子类,然后覆盖实现父类加密的方法,在里面使用3DES来实现即可,已有的实现不需要做任何变化。

    另外一种情况是想要给两个登录模块都扩展同一个功能,这种情况多属于需要修改模板方法的算法骨架的情况,应该尽量避免,但是万一前面没有考虑周全,后来出现了这种情况,怎么办呢?最好就是重构,也就是考虑修改算法骨架,尽量不要去找其它的替代方式,替代的方式也许能把功能实现了,但是会破坏整个程序的结构。

还有一种情况是既需要加入新的功能,也需要新的数据。比如:现在对于普通人员登录,要实现一个加强版,要求登录人员除了编号和密码外,还需要提供注册时留下的验证问题和验证答案,验证问题和验证答案是记录在数据库中的,不是验证码,一般Web开发中登录使用的验证码会放到session中,这里不去讨论它。

       假如现在就要进行如此的扩展,应该怎么实现呢?由于需要一些其它的数据,那么就需要扩展LoginModel,加入自己需要的数据;同时可能需要覆盖由父类提供的一些公共的方法,来实现新的功能。

       还是看看代码示例吧,会比较清楚。

       首先呢,需要扩展LoginModel,把具体功能需要的数据封装起来,只是增加父类没有的数据就可以了,示例代码如下:

/**

 * 封装进行登录控制所需要的数据,在公共数据的基础上,

 * 添加具体模块需要的数据

 */

public class NormalLoginModel extends LoginModel{

    /**

     * 密码验证问题

     */

    private String question;

    /**

     * 密码验证答案

     */

    private String answer;

    public String getQuestion() {

       return question;

    }

    public void setQuestion(String question) {

       this.question = question;

    }

    public String getAnswer() {

       return answer;

    }

    public void setAnswer(String answer) {

       this.answer = answer;

    }

}

       其次呢,就是提供新的登录模块控制实现,示例代码如下:

/**

 * 普通用户登录控制加强版的逻辑处理

 */

public class NormalLogin2 extends LoginTemplate{

    public LoginModel findLoginUser(String loginId) {

       // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象

       //注意一点:这里使用的是自己需要的数据模型了

       NormalLoginModel nlm = new NormalLoginModel();

       nlm.setLoginId(loginId);

       nlm.setPwd("testpwd");

       nlm.setQuestion("testQuestion");

       nlm.setAnswer("testAnswer");

      

       return nlm;

    }

    public boolean match(LoginModel lm,LoginModel dbLm){

       //这个方法需要覆盖,因为现在进行登录控制的时候,

       //需要检测4个值是否正确,而不仅仅是缺省的2个

      

       //先调用父类实现好的,检测编号和密码是否正确

       boolean f1 = super.match(lm, dbLm);

       if(f1){

           //如果编号和密码正确,继续检查问题和答案是否正确

          

           //先把数据转换成自己需要的数据

           NormalLoginModel nlm = (NormalLoginModel)lm;

           NormalLoginModel dbNlm = (NormalLoginModel)dbLm;

           //检查问题和答案是否正确

           if(dbNlm.getQuestion().equals(nlm.getQuestion())

                  && dbNlm.getAnswer().equals(nlm.getAnswer())){

              return true;

           }

       }

       return false;

    }  

}

看看这个时候的测试,示例代码如下:

public class Client {

    public static void main(String[] args) {

       //准备登录人的信息

       NormalLoginModel nlm = new NormalLoginModel();

       nlm.setLoginId("testUser");

       nlm.setPwd("testpwd");

       nlm.setQuestion("testQuestion");

       nlm.setAnswer("testAnswer");

       //准备用来进行判断的对象

       LoginTemplate lt3 = new NormalLogin2();

       //进行登录测试

       boolean flag3 = lt3.login(nlm);

       System.out.println("可以进行普通人员加强版登录="+flag3);

    }

}

       运行看看,能实现功能吗?好好测试体会一下,看看是如何扩展功能的。

16.3.2  模板的写法

在实现模板的时候,到底哪些方法实现在模板上呢?模板能不能全部实现了,也就是模板不提供抽象方法呢?当然,就算没有抽象方法,模板一样可以定义成为抽象类。

    通常在模板里面包含如下操作类型:

  • 模板方法:就是定义算法骨架的方法 。
  • 具体的操作:在模板中直接实现某些步骤的方法,通常这些步骤的实现算法是固定的,而且是不怎么变化的,因此就可以当作公共功能实现在模板里面。如果不需提供给子类访问这些方法的话,还可以是private的。这样一来,子类的实现就相对简单些。如果是子类需要访问,可以把这些方法定义为protected final的,因为通常情况下,这些实现不能够被子类覆盖和改变了。
  • 具体的AbstractClass操作:在模板中实现某些公共功能,可以提供给子类使用,一般不是具体的算法步骤的实现,只是一些辅助的公共功能。
  • 原语操作:就是在模板中定义的抽象操作,通常是模板方法需要调用的操作,是必需的操作,而且在父类中还没有办法确定下来如何实现,需要子类来真正实现的方法。
  • 钩子操作:在模板中定义,并提供默认实现的操作。这些方法通常被视为可扩展的点,但不是必须的,子类可以有选择的覆盖这些方法,以提供新的实现来扩展功能。比如:模板方法中定义了5步操作,但是根据需要,某一种具体的实现只需要其中的1、2、3这几个步骤,因此它就只需要覆盖实现1、2、3这几个步骤对应的方法。那么4和5步骤对应的方法怎么办呢,由于有默认实现,那就不用管了。也就是说钩子操作是可以被扩展的点,但不是必须的。
  • Factory Method:在模板方法中,如果需要得到某些对象实例的话,可以考虑通过工厂方法模式来获取,把具体的构建对象的实现延迟到子类中去。

总结起来,一个较为完整的模板定义示例,示例代码如下:

/**

 * 一个较为完整的模版定义示例

 */

public abstract class AbstractTemplate {

    /**

     * 模板方法,定义算法骨架

     */

    public final void templateMethod(){

       //第一步

       this.operation1();

       //第二步      

       this.operation2();

       //第三步

       this.doPrimitiveOperation1();

       //第四步

       this.doPrimitiveOperation2();

       //第五步

       this.hookOperation1();

    }

    /**

     * 具体操作1,算法中的步骤,固定实现,而且子类不需要访问

     */

    private void operation1(){

       //在这里具体的实现

    }

    /**

     * 具体操作2,算法中的步骤,固定实现,子类可能需要访问,

     * 当然也可以定义成protected的,不可以被覆盖,因此是final

     */

    protected final void operation2(){

       //在这里具体的实现

    }

    /**

     * 具体的AbstractClass操作,子类的公共功能,

     * 但通常不是具体的算法步骤

     */

    protected void commonOperation(){

        //在这里具体的实现

    }

    /**

     * 原语操作1,算法中的必要步骤,父类无法确定如何真正实现,需要子类来实现

     */

    protected abstract void doPrimitiveOperation1();

    /**

     * 原语操作2,算法中的必要步骤,父类无法确定如何真正实现,需要子类来实现

     */

    protected abstract void doPrimitiveOperation2();

    /**

     * 钩子操作,算法中的步骤,不一定需要,提供缺省实现

     * 由子类选择并具体实现

     */

    protected void hookOperation1(){

       //在这里提供缺省的实现

    }

    /**

     * 工厂方法,创建某个对象,这里用Object代替了,在算法实现中可能需要

     * @return 创建的某个算法实现需要的对象

     */

    protected abstract Object createOneObject();

}

       对于上面示例的模板写法,其中定义成为protected的方法,可以根据需要进行调整,如果是允许所有的类都可以访问这些方法,那么可以把它们定义成为public的,如果只是子类需要访问这些方法,那就使用protected的,都是正确的写法。

16.3.3  Java回调与模板方法模式

模板方法模式的一个目的,就在于让其它类来扩展或具体实现在模板中固定的算法骨架中的某些算法步骤。在标准的模板方法模式实现中,主要是使用继承的方式,来让父类在运行期间可以调用到子类的方法。

其实在Java开发中,还有另外一个方法可以实现同样的功能或是效果,那就是——Java回调技术,通过回调在接口中定义的方法,调用到具体的实现类中的方法,其本质同样是利用Java的动态绑定技术,在这种实现中,可以不把实现类写成单独的类,而是使用匿名内部类来实现回调方法。

       应用Java回调来实现模板方法模式,在实际开发中使用得也非常多,就算是模板方法模式的一种变形实现吧。

       还是来示例一下,这样会更清楚。为了大家好对比理解,把前面用标准模板方法模式实现的例子,采用Java回调来实现一下。

(1)先定义一个模板方法需要的回调接口

在这个接口中需要把所有可以被扩展的方法都要定义出来。实现的时候,可以不扩展,直接转调模板中的默认实现,但是不能不定义出来,因为是接口,不定义出来,对于想要扩展这些功能的地方就没有办法了。示例代码如下:

/**

 * 登录控制的模板方法需要的回调接口,需要把所有需要的接口方法都定义出来,

 * 或者说是所有可以被扩展的方法都需要被定义出来

 */

public interface LoginCallback {

    /**

     * 根据登录编号来查找和获取存储中相应的数据

     * @param loginId 登录编号

     * @return 登录编号在存储中相对应的数据

     */

    public LoginModel findLoginUser(String loginId);

    /**

     * 对密码数据进行加密

     * @param pwd 密码数据

     * @param template LoginTemplate对象,通过它来调用在

     *             LoginTemplate中定义的公共方法或缺省实现

     * @return 加密后的密码数据

     */

    public String encryptPwd(String pwd,LoginTemplate template);

    /**

     * 判断用户填写的登录数据和存储中对应的数据是否匹配得上

     * @param lm 用户填写的登录数据

     * @param dbLm 在存储中对应的数据

     * @param template LoginTemplate对象,通过它来调用在

     *             LoginTemplate中定义的公共方法或缺省实现

     * @return true表示匹配成功,false表示匹配失败

     */

    public boolean match(LoginModel lm,LoginModel dbLm

,LoginTemplate template);

}

(2)这里使用的LoginModel跟以前没有任何变化,就不去赘述了。

(3)该来定义登录控制的模板了,它的变化相对较多,大致有以下一些:

  • 不再是抽象的类了,所有的抽象方法都去掉了
  • 对模板方法就是login的那个方法,添加一个参数,传入回调接口
  • 在模板方法实现中,除了在模板中固定的实现外,所有可以被扩展的方法,都应该通过回调接口进行调用

示例代码如下:

/**

 *  登录控制的模板

 */

public class LoginTemplate {

    /**

     * 判断登录数据是否正确,也就是是否能登录成功

     * @param lm 封装登录数据的Model

     * @param callback LoginCallback对象

     * @return true表示登录成功,false表示登录失败

     */

    public final boolean login(LoginModel lm,LoginCallback callback){

       //1:根据登录人员的编号去获取相应的数据

       LoginModel dbLm = callback.findLoginUser(lm.getLoginId());

       if(dbLm!=null){

           //2:对密码进行加密

           String encryptPwd =

callback.encryptPwd(lm.getPwd(),this);

           //把加密后的密码设置回到登录数据模型里面

           lm.setPwd(encryptPwd);

           //3:判断是否匹配

           return callback.match(lm, dbLm,this);

       }

       return false;

    }

    /**

     * 对密码数据进行加密

     * @param pwd 密码数据

     * @return 加密后的密码数据

     */

    public String encryptPwd(String pwd){

       return pwd;

    }

    /**

     * 判断用户填写的登录数据和存储中对应的数据是否匹配得上

     * @param lm 用户填写的登录数据

     * @param dbLm 在存储中对应的数据

     * @return true表示匹配成功,false表示匹配失败

     */

    public boolean match(LoginModel lm,LoginModel dbLm){

       if(lm.getLoginId().equals(dbLm.getLoginId())

              && lm.getPwd().equals(dbLm.getPwd())){

           return true;

       }

       return false;

    }

}

(4)由于是直接在调用的地方传入回调的实现,通常可以通过匿名内部类的方式来实现回调接口,当然实现成为具体类也是可以的。如果采用匿名内部类的方式来使用模板,那么就不需要原来的NormalLogin和WorkerLogin了。

(5)写个客户端来测试看看,客户端需要使用匿名内部类来实现回调接口,并实现其中想要扩展的方法,示例代码如下:

public class Client {

    public static void main(String[] args) {

       //准备登录人的信息

       LoginModel lm = new LoginModel();

       lm.setLoginId("admin");

       lm.setPwd("workerpwd");

       //准备用来进行判断的对象

       LoginTemplate lt = new LoginTemplate();

      

       //进行登录测试,先测试普通人员登录

       boolean flag = lt.login(lm,new LoginCallback(){

           public String encryptPwd(String pwd

, LoginTemplate template) {

              //自己不需要,直接转调模板中的默认实现

              return template.encryptPwd(pwd);

           }

           public LoginModel findLoginUser(String loginId) {

              // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象

              LoginModel lm = new LoginModel();

              lm.setLoginId(loginId);

              lm.setPwd("testpwd");

              return lm;

           }

           public boolean match(LoginModel lm, LoginModel dbLm,

                  LoginTemplate template) {

              //自己不需要覆盖,直接转调模板中的默认实现

              return template.match(lm, dbLm);

           }

       });

       System.out.println("可以进行普通人员登录="+flag);

      

       //测试工作人员登录

       boolean flag2 = lt.login(lm,new LoginCallback(){

           public String encryptPwd(String pwd

, LoginTemplate template) {

              //覆盖父类的方法,提供真正的加密实现

              //这里对密码进行加密,比如使用:MD53DES等等,省略了

              System.out.println("使用MD5进行密码加密");

              return pwd;

           }

           public LoginModel findLoginUser(String loginId) {

              // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象

              LoginModel lm = new LoginModel();

              lm.setLoginId(loginId);

              lm.setPwd("workerpwd");

              return lm;

           }

           public boolean match(LoginModel lm, LoginModel dbLm,

                  LoginTemplate template) {

              //自己不需要覆盖,直接转调模板中的默认实现

              return template.match(lm, dbLm);

           }         

       });   

       System.out.println("可以登录工作平台="+flag2);

    }

}

       运行一下,看看效果是不是跟前面采用继承的方式实现的结果是一样的,然后好好比较一下这两种实现方式。

(6)简单小结一下,对于模板方法模式的这两种实现方式:

  • 使用继承的方式,抽象方法和具体实现的关系,是在编译期间静态决定的,是类级的关系;使用Java回调,这个关系是在运行期间动态决定的,是对象级的关系。
  • 相对而言,使用回调机制会更灵活,因为Java是单继承的,如果使用继承的方式,对于子类而言,今后就不能继承其它对象了,而使用回调,是基于接口的。
        从另一方面说,回调机制是通过委托的方式来组合功能,它的耦合强度要比继承低一些,这会给我们更多的灵活性。比如某些模板实现的方法,在回调实现的时候可以不调用模板中的方法,而是调用其它实现中的某些功能,也就是说功能不再局限在模板和回调实现上了,可以更灵活组织功能。
  • 相对而言,使用继承方式会更简单点,因为父类提供了实现的方法,子类如果不想扩展,那就不用管。如果使用回调机制,回调的接口需要把所有可能被扩展的方法都定义进去,这就导致实现的时候,不管你要不要扩展,你都要实现这个方法,哪怕你什么都不做,只是转调模板中已有的实现,都要写出来。

事实上,在前面讲命令模式的时候也提到了Java回调,还通过退化命令模式来实现了Java回调的功能,所以也有这样的说法:命令模式可以作为模板方法模式的一种替代实现,那就是因为可以使用Java回调来实现模板方法模式。

16.3.4  典型应用:排序

       模板方法模式的一个非常典型的应用,就是实现排序的功能。至于有些朋友认为排序是策略模式的体现,这很值得商榷。先来看看在Java中排序功能的实现,然后再来说明为什么排序的实现主要体现了模板方法模式,而非策略模式。

       在java.util包中,有一个Collections类,它里面实现了对列表排序的功能,它提供了一个静态的sort方法,接受一个列表和一个Comparator接口的实例,这个方法实现的大致步骤是:

  • 先把列表转换成为对象数组
  • 通过Arrays的sort方法来对数组进行排序,传入Comparator接口的实例
  • 然后再把排好序的数组的数据设置回到原来的列表对象中去

这其中的算法步骤是固定的,也就是算法骨架是固定的了,只是其中具体比较数据大小的步骤,需要由外部来提供,也就是传入的Comparator接口的实例,就是用来实现数据比较的,在算法内部会通过这个接口来回调具体的实现。

如果Comparator接口的compare()方法返回一个小于0的数,表示被比较的两个对象中,前面的对象小于后面的对象;如果返回一个等于0的数,表示被比较的两个对象相等;如果返回一个大于0的数,表示被比较的两个对象中,前面的对象大于后面的对象。

下面一起看看使用Collections来对列表排序的例子,假如现在要实现对一个拥有多个用户数据模型的列表进行排序。

(1)当然,先要定义出封装用户数据的对象模型来,示例代码如下:

/**

 * 用户数据模型

 */

public class UserModel {

    private String userId,name;

    private int age;

    public UserModel(String userId,String name,int age) {

       this.userId = userId;

       this.name = name;

       this.age = age;

    }

    public String getUserId() {

       return userId;

    }

    public String getName() {

        return name;

    }

    public int getAge() {

       return age;

    }

    public String toString(){

       return "userId="+userId+",name="+name+",age="+age;

    }

}

(2)直接使用Collections来排序,写个客户端来测试一下,示例代码如下:

public class Client {

    public static void main(String[] args) {

       //准备要测试的数据

       UserModel um1 = new UserModel("u1","user1",23);

       UserModel um2 = new UserModel("u2","user2",22);

       UserModel um3 = new UserModel("u3","user3",21);

       UserModel um4 = new UserModel("u4","user4",24);

       //添加到列表中

       List<UserModel> list = new ArrayList<UserModel>();

       list.add(um1);

       list.add(um2);

       list.add(um3);

       list.add(um4);

      

       System.out.println("排序前---------------------〉");

       printList(list);

       //实现比较器,也可以单独用一个类来实现

       Comparator c = new Comparator(){

           public int compare(Object obj1, Object obj2) {

              //假如实现按照年龄升序排序

              UserModel tempUm1 = (UserModel)obj1;

              UserModel tempUm2 = (UserModel)obj2;

              if(tempUm1.getAge() > tempUm2.getAge()){

                  return 1;

              }else if(tempUm1.getAge() == tempUm2.getAge()){

                  return 0;

              }else if(tempUm1.getAge() < tempUm2.getAge()){

                  return -1;

              }

              return 0;

           }};

      

       //排序 

       Collections.sort(list,c);

      

       System.out.println("排序后---------------------〉");

       printList(list);

    }

    private static void printList(List<UserModel> list){

       for(UserModel um : list){

           System.out.println(um);

       }

    }

}

运行一下,结果如下所示:

排序前---------------------〉

userId=u1,name=user1,age=23

userId=u2,name=user2,age=22

userId=u3,name=user3,age=21

userId=u4,name=user4,age=24

排序后---------------------〉

userId=u3,name=user3,age=21

userId=u2,name=user2,age=22

userId=u1,name=user1,age=23

userId=u4,name=user4,age=24

(3)小结一下

       看了上面的示例,你会发现,究竟列表会按照什么标准来排序,完全是依靠Comparator的具体实现,上面实现的是按照年龄升序排列,你可以尝试修改这个排序的比较器,那么得到的结果就会不一样了。

也就是说,排序的算法是已经固定了的,只是进行排序比较的这一个步骤,由外部来实现,我们就可以通过修改这个步骤的实现,从而实现不同的排序方式。因此从排序比较这个功能来看,是策略模式的体现。

但是请注意一点,你只是修改的排序的比较方式,并不是修改了整个排序的算法,事实上,现在Collections的sort()方法使用的是合并排序的算法,无论你怎么修改比较器的实现,sort()方法实现的算法是不会改变的,不可能变成了冒泡排序或是其它的排序算法。

(4)排序,到底是模板方法模式的实例,还是策略模式的实例,到底哪个说法更合适?

       认为是策略模式的实例的理由:

  • 首先上面的排序实现,并没有如同标准的模板方法模式那样,使用子类来扩展父类,至少从表面上看不太像模板方法模式;
  • 其次排序使用的Comparator的实例,可以看成是不同的算法实现,在具体排序时,会选择使用不同的Comparator实现,就相当于是在切换算法的实现。

因此认为排序是策略模式的实例。

       认为是模板方法模式的实例的理由:

  • 首先,模板方法模式的本质是固定算法骨架,虽然使用继承是标准的实现方式,但是通过回调来实现,也不能说这就不是模板方法模式;
  • 其次,从整体程序上看,排序的算法并没有改变,不过是某些步骤的实现发生了变化,也就是说通过Comparator来切换的是不同的比较大小的实现,相对于整个排序算法而言,它不过是其中的一个步骤而已。

因此认为是模板方法模式的实例。

       总结语:

排序的实现,实际上组合使用了模板方法模式和策略模式,从整体来看是模板方法模式,但到了局部,比如排序比较算法的实现上,就是使用的策略模式了。

至于排序具体属于谁的实例,这或许是个仁者见仁、智者见智的事情,我们也不想做老好人,我们倾向于说:排序是模板方法模式的实例。毕竟设计模式的东西,要从整体上、设计上、从本质上去看待问题,而不能从表面上或者是局部来看待问题。

16.3.5  实现通用增删改查

    对于实现通用的增删改查的功能,基本上是每个做企业级应用系统的公司都有的功能,实现的方式也是多种多样,一种很常见的设计就是泛型加上模板方法模式,再加上使用Java回调技术,尤其是在使用Spring和Hibernate等流行框架的应用系统中很是常见。

    为了突出主题,以免分散大家的注意力,我们不去使用Spring和Hibernate这样的流行框架,也不去使用泛型,只用模板方法模式来实现一个简单的、用JDBC实现的通用增删改查的功能。

    先在数据库中定义一个演示用的表,演示用的是Oracle数据库,其实你可以用任意的数据库,只是数据类型要做相应的调整,简单的数据字典如下:表名是tbl_user

字段

名称

类型、长度

主外键

Uuid

编号

Varchar2(10)

主键

Name

姓名

Varchar2(20)

 

Age

年龄

Number(3)

 

(1)定义相应的数据对象来描述数据,示例代码如下:

/**

 * 描述用户的数据模型

 */

public class UserModel {

    private String uuid;

    private String name;

    private int age;

    public String getUuid() {

       return uuid;

    }

    public void setUuid(String uuid) {

       this.uuid = uuid;

    }

    public String getName() {

       return name;

    }

    public void setName(String name) {

       this.name = name;

    }

    public int getAge() {

       return age;

    }

    public void setAge(int age) {

       this.age = age;

    }  

    public String toString(){

       return "uuid="+uuid+",name="+name+",age="+age;

    }

}

(2)定义一个用于封装通用查询数据的查询用的数据模型,由于这个查询数据模型和上面定义的数据模型有很大一部分是相同的,因此让这个查询模型继承上面的数据模型,然后添加上多出来的查询条件。示例代码如下:

/**

 * 描述查询用户的条件数据的模型

 */

public class UserQueryModel extends UserModel{

    /**

     * 年龄是一个区间查询,也就是年龄查询的条件可以是:

     * age >= 条件值1  and  age <= 条件值2

     * UserModel中的age当作条件值1

     * 这里定义的age2当作条件值2

     */

    private int age2;

    public int getAge2() {

       return age2;

    }

    public void setAge2(int age2) {

       this.age2 = age2;

    }

}

(3)为了让大家能更好的理解这个通用的实现,先不去使用模板方法模式,直接使用JDBC来实现增删改查的功能。

所有的方法都需要和数据库进行连接,因此先把和数据库连接的公共方法定义出来,没有使用连接池,用最简单的JDBC自己连接,示例代码如下:

    /**

     * 获取与数据库的连接

     * @return 数据库连接

     * @throws Exception

     */

    private Connection getConnection()throws Exception{

       Class.forName("你用的数据库对应的JDBC驱动类");

       return DriverManager.getConnection(

              "连接数据库的URL",

              "用户名","密码");

    }

使用纯JDBC来实现新增的功能,示例代码如下:

    public void create(UserModel um) {

       Connection conn = null;

       try {

           conn = this.getConnection();

           String sql = "insert into tbl_user values(?,?,?)";

           PreparedStatement pstmt = conn.prepareStatement(sql);

           pstmt.setString(1, um.getUuid());

           pstmt.setString(2, um.getName());

           pstmt.setInt(3, um.getAge());

 

           pstmt.executeUpdate();

 

           pstmt.close();

       } catch (Exception err) {

           err.printStackTrace();

       } finally {

           try {

              conn.close();

           } catch (SQLException e) {

              e.printStackTrace();

           }

       }

    }

    修改和删除的功能跟新增差不多,只是sql不同,还有设置sql中变量值不同,这里就不去写了。

    接下来看看查询方面的功能,查询方面只做一个通用的查询实现,其它查询的实现基本上也差不多,示例代码如下:

    public Collection getByCondition(UserQueryModel uqm){

       Collection col = new ArrayList();

       Connection conn = null;

       try{

           conn = this.getConnection();

           String sql = "select * from tbl_user where 1=1 ";

           sql = this.prepareSql(sql, uqm);

 

           PreparedStatement pstmt = conn.prepareStatement(sql);

           this.setValue(pstmt, uqm);

           ResultSet rs = pstmt.executeQuery();

           while(rs.next()){

              col.add(this.rs2Object(rs));

           }

 

             rs.close();

             pstmt.close();

       }catch(Exception err){

           err.printStackTrace();

       }finally{

           try {

              conn.close();

           } catch (SQLException e) {

              e.printStackTrace();

           }

       }

       return col;

    }

    /**

     * 为通用查询动态的拼接sql的条件部分,基本思路是:

     * 如果用户填写了相应的条件,那么才在sql中添加对应的条件

     * @param sql sql的主干部分

     * @param uqm 封装查询条件的数据模型

     * @return 拼接好的sql语句

     */

    private String prepareSql(String sql,UserQueryModel uqm){

      

 

 

StringBuffer buffer = new StringBuffer();

       buffer.append(sql);

       //绝对匹配

       if(uqm.getUuid()!=null&& uqm.getUuid().trim().length()>0){

           buffer.append(" and uuid=? ");

       }

       //模糊匹配

       if(uqm.getName()!=null&& uqm.getName().trim().length()>0){

           buffer.append(" and name like ? ");

       }

       //区间匹配

       if(uqm.getAge() > 0){

           buffer.append(" and age >=? ");

       }

       if(uqm.getAge2() > 0){

           buffer.append(" and age <=? ");

        }

       return buffer.toString();

    }

    /**

     * 为通用查询的sql动态设置条件的值

     * @param pstmt 预处理查询sql的对象

     * @param uqm 封装查询条件的数据模型

     * @throws Exception

     */

    private void setValue(PreparedStatement pstmt

,UserQueryModel uqm)throws Exception{

       int count = 1;

       if(uqm.getUuid()!=null

&& uqm.getUuid().trim().length()>0){

           pstmt.setString(count, uqm.getUuid());

           count++;

       }

       if(uqm.getName()!=null

&& uqm.getName().trim().length()>0){

           pstmt.setString(count, "%"+uqm.getName()+"%");

           count++;

       }

       if(uqm.getAge() > 0){

           pstmt.setInt(count, uqm.getAge());

           count++;

       }

       if(uqm.getAge2() > 0){

           pstmt.setInt(count, uqm.getAge2());

           count++;

       }

    }

    /**

     * 把查询返回的结果集转换成为对象

     * @param rs 查询返回的结果集

     * @return 查询返回的结果集转换成为对象

     * @throws Exception

     */

    private UserModel rs2Object(ResultSet rs)throws Exception{

       UserModel um = new UserModel();

       String uuid = rs.getString("uuid");

       String name = rs.getString("name");

       int age = rs.getInt("age");

      

       um.setAge(age);

       um.setName(name);

       um.setUuid(uuid);

      

       return um;

    }

(4)基本的JDBC实现写完了,该来看看如何把模板方法模式用上了。模板方法是要定义算法的骨架,而具体步骤的实现还是由子类来完成,因此把固定的算法骨架抽取出来,就成了使用模板方法模式的重点了。

       首先来观察新增、修改、删除的功能,发现哪些是固定的,哪些是变化的呢?分析发现变化的只有Sql语句,还有为Sql中的“?”设置值的语句,真正执行sql的过程是差不多的,是不变化的。

       再来观察查询的方法,查询的过程是固定的,变化的除了有Sql语句、为Sql中的“?”设置值的语句之外,还多了一个如何把查询回来的结果集转换成对象集的实现。

    好了,找到变与不变之处,就可以来设计模板了,先定义出增删改查各自的实现步骤来,也就是定义好各自的算法骨架,然后把变化的部分定义成为原语操作或钩子操作,如果一定要子类实现的那就定义成为原语操作;在模板中提供默认实现,且不强制子类实现的功能定义成为钩子操作就可以了。

另外,来回需要传递数据,由于是通用的方法,就不能用具体的类型了,又不考虑泛型,那么就定义成Object类型好了。

根据上面的思路,一个简单的、能实现对数据进行增删改查的模板就可以实现出来了,完整的示例代码如下:

/**

 * 一个简单的实现JDBC增删改查功能的模板

 */

public abstract class JDBCTemplate {

    /**

     * 定义当前的操作类型是新增

     */

    protected final static int TYPE_CREATE = 1;

    /**

     * 定义当前的操作类型是修改

     */

    protected final static int TYPE_UPDATE = 2;

    /**

     * 定义当前的操作类型是删除

     */

    protected final static int TYPE_DELETE = 3;

    /**

     * 定义当前的操作类型是按条件查询

     */

    protected final static int TYPE_CONDITION = 4;

   

/*---------------------模板方法---------------------*/  

    /**

     * 实现新增的功能

     * @param obj 需要被新增的数据对象

     */

    public final void create(Object obj){

       //1:获取新增的sql

       String sql = this.getMainSql(TYPE_CREATE);

       //2:调用通用的更新实现

       this.executeUpdate(sql, TYPE_CREATE,obj);

    }

    /**

     * 实现修改的功能

     * @param obj 需要被修改的数据对象

     */

    public final void update(Object obj){

       //1:获取修改的sql

       String sql = this.getMainSql(TYPE_UPDATE);

       //2:调用通用的更新实现

       this.executeUpdate(sql, TYPE_UPDATE,obj);

    }

    /**

     * 实现删除的功能

     * @param obj 需要被删除的数据对象

     */

    public final void delete(Object obj){

       //1:获取删除的sql

       String sql = this.getMainSql(TYPE_DELETE);

       //2:调用通用的更新实现

       this.executeUpdate(sql, TYPE_DELETE,obj);

    }

    /**

     * 实现按照条件查询的功能

     * @param qm 封装查询条件的数据对象

     * @return 符合条件的数据对象集合

     */

    public final Collection getByCondition(Object qm){

       //1:获取查询的sql

       String sql = this.getMainSql(TYPE_CONDITION);

       //2:调用通用的查询实现

       return this.getByCondition(sql, qm);

    }

   

/*---------------------原语操作---------------------*/      

    /**

     * 获取操作需要的主干sql

     * @param type 操作类型

     * @return 操作对应的主干sql

     */

    protected abstract String getMainSql(int type);

    /**

     * 为更新操作的sql中的"?"设置值

     * @param type 操作类型

     * @param pstmt PreparedStatement对象

     * @param obj 操作的数据对象

     * @throws Exception

     */

    protected abstract void setUpdateSqlValue(int type,

PreparedStatement pstmt,Object obj) throws Exception;

    /**

     * 为通用查询动态的拼接sql的条件部分,基本思路是:

     * 只有用户填写了相应的条件,那么才在sql中添加对应的条件

     * @param sql sql的主干部分

     * @param qm 封装查询条件的数据模型

     * @return 拼接好的sql语句

     */

    protected abstract String prepareQuerySql(String sql,Object qm);

    /**

     * 为通用查询的sql动态设置条件的值

     * @param pstmt 预处理查询sql的对象

     * @param qm 封装查询条件的数据模型

     * @throws Exception

     */

    protected abstract void setQuerySqlValue(

PreparedStatement pstmt,Object qm)throws Exception;

    /**

     * 把查询返回的结果集转换成为数据对象

     * @param rs 查询返回的结果集

     * @return 查询返回的结果集转换成为数据对象

     * @throws Exception

     */

    protected abstract Object rs2Object(ResultSet rs)throws Exception;

   

/*---------------------钩子操作---------------------*/      

    /**

     * 连接数据库的默认实现,可以被子类覆盖

     * @return 数据库连接

     * @throws Exception

     */

    protected Connection getConnection()throws Exception{

       Class.forName("你用的数据库对应的JDBC驱动类");

       return DriverManager.getConnection(

              "连接数据库的URL",

              "用户名","密码");

    }

/**

     * 执行查询

     * @param sql 查询的主干sql语句

     * @param qm 封装查询条件的数据模型

     * @return 查询后的结果对象集合

     */

    protected Collection getByCondition(String sql,Object qm){

       Collection col = new ArrayList();

       Connection conn = null;

       try{

           //调用钩子方法

           conn = this.getConnection();

           //调用原语操作

           sql = this.prepareQuerySql(sql, qm);

           PreparedStatement pstmt = conn.prepareStatement(sql);

           //调用原语操作

           this.setQuerySqlValue(pstmt, qm);

           ResultSet rs = pstmt.executeQuery();

           while(rs.next()){

              //调用原语操作

              col.add(this.rs2Object(rs));

           }

 

           rs.close();

           pstmt.close();

       }catch(Exception err){

           err.printStackTrace();

       }finally{

           try {

              conn.close();

           } catch (SQLException e) {

              e.printStackTrace();

           }

       }

       return col;

    }

    /**

     * 执行更改数据的sql语句,包括增删改的功能

     * @param sql 需要执行的sql语句

     * @param callback 回调接口,回调为sql语句赋值的方法

     */

    protected void executeUpdate(String sql,int type,Object obj){

       Connection conn = null;

       try{

           //调用钩子方法        

           conn = this.getConnection();

           PreparedStatement pstmt = conn.prepareStatement(sql);

           //调用原语操作

           this.setUpdateSqlValue(type,pstmt,obj);         

           pstmt.executeUpdate();         

           pstmt.close();

       }catch(Exception err){

           err.printStackTrace();

       }finally{

           try {

              conn.close();

           } catch (SQLException e) {

              e.printStackTrace();

           }

       }

    }

}

(5)简单但是可以通用的JDBC模板做好了,看看如何使用这个模板来实现具体的增删改查功能,示例代码如下:

/**

 * 具体的实现用户管理的增删改查功能

 */

public class UserJDBC extends JDBCTemplate{  

    protected String getMainSql(int type) {

       //根据操作类型,返回相应的主干sql语句

       String sql = "";

       if(type == TYPE_CREATE){

           sql = "insert into tbl_user values(?,?,?)";

       }else if(type == TYPE_DELETE){

           sql = "delete from tbl_user where uuid=?";

       }else if(type == TYPE_UPDATE){

           sql = "update tbl_user set name=?,age=? where uuid=?";

       }else if(type == TYPE_CONDITION){

           sql = "select * from tbl_user where 1=1 ";

       }

       return sql;

    }

    protected void setUpdateSqlValue(int type

, PreparedStatement pstmt,Object obj) throws Exception{

       //设置增、删、改操作的sql中"?"对应的值

       if(type == TYPE_CREATE){

           this.setCreateValue(pstmt, (UserModel)obj);

       }else if(type == TYPE_DELETE){

           this.setDeleteValue(pstmt, (UserModel)obj);

       }else if(type == TYPE_UPDATE){

           this.setUpdateValue(pstmt, (UserModel)obj);

       }

    }

    protected Object rs2Object(ResultSet rs)throws Exception{

       UserModel um = new UserModel();

       String uuid = rs.getString("uuid");

       String name = rs.getString("name");

       int age = rs.getInt("age");

      

       um.setAge(age);

       um.setName(name);

       um.setUuid(uuid);

      

       return um;

    }

    protected String prepareQuerySql(String sql,Object qm){

       UserQueryModel uqm = (UserQueryModel)qm;

       StringBuffer buffer = new StringBuffer();

       buffer.append(sql);

       if(uqm.getUuid()!=null&& uqm.getUuid().trim().length()>0){

           buffer.append(" and uuid=? ");

       }

       if(uqm.getName()!=null&& uqm.getName().trim().length()>0){

           buffer.append(" and name like ? ");

       }

       if(uqm.getAge() > 0){

           buffer.append(" and age >=? ");

       }

       if(uqm.getAge2() > 0){

           buffer.append(" and age <=? ");

       }

       return buffer.toString();

    }

    protected void setQuerySqlValue(PreparedStatement pstmt

,Object qm)throws Exception{

       UserQueryModel uqm = (UserQueryModel)qm;

       int count = 1;

       if(uqm.getUuid()!=null&& uqm.getUuid().trim().length()>0){

           pstmt.setString(count, uqm.getUuid());

           count++;

       }

       if(uqm.getName()!=null&& uqm.getName().trim().length()>0){

           pstmt.setString(count, "%"+uqm.getName()+"%");

           count++;

       }

       if(uqm.getAge() > 0){

           pstmt.setInt(count, uqm.getAge());

           count++;

       }

       if(uqm.getAge2() > 0){

           pstmt.setInt(count, uqm.getAge2());

           count++;

       }

    }

    private void setCreateValue(PreparedStatement pstmt,UserModel um)throws Exception{

       pstmt.setString(1, um.getUuid());

       pstmt.setString(2, um.getName());

       pstmt.setInt(3, um.getAge());

    }

    private void setUpdateValue(PreparedStatement pstmt,UserModel um)throws Exception{

       pstmt.setString(1, um.getName());

       pstmt.setInt(2, um.getAge());

       pstmt.setString(3, um.getUuid());

    }

    private void setDeleteValue(PreparedStatement pstmt,UserModel um)throws Exception{

       pstmt.setString(1, um.getUuid());

    }

}

看到这里,可能有些朋友会想,为何不把准备sql的方法、为sql中“?”赋值的方法、还有结果集映射成为对象的方法也做成公共的呢?

其实这些方法是可以考虑做成公共的,用反射机制就可以实现,但是这里为了突出模板方法模式的使用,免得加的东西太多,把大家搞迷惑了。

事实上,用模板方法加上泛型再加上反射的技术,就可以实现可重用的,使用模板时几乎不用再写代码的数据层实现,这里就不去展开了。

(6)享受的时刻到了,来写个客户端,使用UserJDBC的实现,示例代码如下:

public class Client {

    public static void main(String[] args) {

       UserJDBC uj = new UserJDBC();

       //先新增几条

       UserModel um1 = new UserModel();

       um1.setUuid("u1");

       um1.setName("张三");

       um1.setAge(22);     

       uj.create(um1);     

      

       UserModel um2 = new UserModel();

       um2.setUuid("u2");

       um2.setName("李四");

       um2.setAge(25);     

       uj.create(um2);

      

       UserModel um3 = new UserModel();

       um3.setUuid("u3");

       um3.setName("王五");

       um3.setAge(32);     

       uj.create(um3);

      

       //测试修改

       um3.setName("王五被改了");

       um3.setAge(35);

       uj.update(um3);

      

       //测试查询

       UserQueryModel uqm = new UserQueryModel();

       uqm.setAge(20);

       uqm.setAge2(36);

       Collection<UserModel> col = uj.getByCondition(uqm);

       for(UserModel tempUm : col){

           System.out.println(tempUm);

       }

    }

}

运行一下,看看结果,看看数据库的值,再好好体会一下是如何实现的。

16.3.6  模板方法模式的优缺点

l          实现代码复用
    模板方法模式是一种实现代码复用的很好的手段。通过把子类的公共功能提炼和抽取,把公共部分放到模板里面去实现。

l          算法骨架不容易升级
    模板方法模式最基本的功能就是通过模板的制定,把算法骨架完全固定下来。事实上模板和子类是非常耦合的,如果要对模板中的算法骨架进行变更,可能就会要求所有相关的子类进行相应的变化。所以抽取算法骨架的时候要特别小心,尽量确保是不会变化的部分才放到模板中。

16.3.7  思考模板方法模式

1:模板方法模式的本质

模板方法模式的本质:固定算法骨架

       模板方法模式主要是通过制定模板,把算法步骤固定下来,至于谁来实现,模板可以自己提供实现,也可以由子类去实现,还可以通过回调机制让其它类来实现。

       通过固定算法骨架,来约束子类的行为,并在特定的扩展点,来让子类进行功能扩展,从而让程序既有很好的复用性,又有较好的扩展性。

2:对设计原则的体现

       模板方法很好的体现了开闭原则和里氏替换原则。

       首先从设计上,先分离变与不变,然后把不变的部分抽取出来,定义到父类里面,比如算法骨架,比如一些公共的、固定的实现等等。这些不变的部分被封闭起来,尽量不去修改它了,要扩展新的功能,那就使用子类来扩展,通过子类来实现可变化的步骤,对于这种新增功能的做法是开放的。

       其次,能够实现统一的算法骨架,通过切换不同的具体实现来切换不同的功能,一个根本原因就是里氏替换原则,遵循这个原则,保证所有的子类实现的是同一个算法模板,并能在使用模板的地方,根据需要,切换不同的具体实现。

3:何时选用模板方法模式

       建议在如下情况中,选用模板方法模式:

  • 需要固定定义算法骨架,实现一个算法的不变的部分,并把可变的行为留给子类来实现的情况
  • 各个子类中具有公共行为,应该抽取出来,集中在一个公共类中去实现,从而避免代码重复
  • 需要控制子类扩展的情况。模板方法模式会在特定的点来调用子类的方法,这样只允许在这些点进行扩展

16.3.8  相关模式

l          模板方法模式和工厂方法模式
    这两个模式可以配合使用。
    模板方法模式可以通过工厂方法来获取需要调用的对象。

l          模板方法模式和策略模式
    这两个模式的功能有些相似,但是是有区别的。
    从表面上看,两个模式都能实现算法的封装,但是模板方法封装的是算法的骨架,这个算法骨架是不变的,变化的是算法中某些步骤的具体实现;而策略模式是把某个步骤的具体实现算法封装起来,所有封装的算法对象是等价的,可以相互替换。
    因此,可以在模板方法中使用策略模式,就是把那些变化的算法步骤通过使用策略模式来实现,但是具体选取哪个策略还是要由外部来确定,而整体的算法步骤,也就是算法骨架就由模板方法来定义了。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics