模板方法(Template Method)模式

提了问题

问题描述:
假设有一个活动,需要经过很多步骤才能完成(A-B-C),某些步骤是固定不变的(A\C),而其他步骤经常发生变化(B)。那么,这样的问题要怎么解决呢?
先从回家过年说起:
春节是中国最热闹的传统节日,对于在外漂泊多时的游子而言,最幸福的事莫过于回家过年。为了回家团圆,我们首先需要购票,然后乘车,最后才能和家人团聚。这里我们编写了一个简单的程序来模拟这个过程(PeopleTravleByTrain.java类)。它有一个方法celebrateSpringFestival()把购票、坐车、欢度春节的逻辑都写在这个方法里。代码如下:

package com.zhanghf.template.method;

/**
 * Created by Administrator on 2017/4/23.
 */
public class PeopleTravleByTrain{
    
    public void celebrateSpringFestival() {
        //买票
        System.out.println("Buying ticket....");
        // 坐火车
        System.out.println("Travelling by train.....");
        //欢度春节
        System.out.println("Happy New Year with family.....");
    }
}

但是,后来我们发现有些人买不到火车票。这样,有些人就需要坐飞机,有些人需要坐大巴,回家方式就产生了变化,而购票和欢度春节却是固定不变的,跟问题描述基本一致。
问题出来了,我们要怎么解决呢。最简单的方式是:从新创建一个类(PeopleTraveleByCoach.java),copy上面的代码,修改坐火车的部分代码就OK了。代码如下:

package com.zhanghf.template.method;

/**
 * Created by Administrator on 2017/4/23.
 */
public class PeopleTraveleByCoach {

    public void celebrateSpringFestival() {
        //买票
        System.out.println("Buying ticket....");
        // 坐汽车
        System.out.println("Travelling by coach.....");
        //欢度春节
        System.out.println("Happy New Year with family.....");
    }
}

看似很好解决了问题,然后我们却发现这里面有很多重复的代码(买票和团圆)。当回家方式越来越多时,重复的代码就会越来越多。这使维护变得非常困难(当需要修改买票、团圆逻辑时,你得每一个类,每一个类的去修改。还得但心是否都改完全了)。这有背于编程原则DRY(Don't Repeat Yourself)

解决问题:

我们知道为了重用代码,可以使用OOP一大特征——继承。把固定不变的方法放在父类,而变化的方法放在子类去实现(这正是模板方法模式的解决方案)。下面先画出UML静态类图,如图所示:

模板方法模式.png

我们使用继承方式重新实现上面例子,代码如下:

package com.zhanghf.template.method;

/**
 * Created by Administrator on 2017/4/23.
 */
public abstract class HappyPeople {

    public void celebrateSpringFestival() {
        //买票
        buyTicket();
        //坐车
        travel();
        //团圆
        celebreate();
    }

    //买票
    protected final void buyTicket() {
        System.out.println("Buyingticket .....");
    }

    //欢度春节
    protected final void celebreate() {
        System.out.println("HappyNew Year with family .....");
    }

    //坐车
    protected abstract void travel();
    
}

子类代码只需继承HappyPeople类,实现里面的travel()方法,就可以实现不同的回家方式的人们了。代码如下:
(PeopleTravelByCoach.java)

package com.zhanghf.template.method;

/**
 * Created by Administrator on 2017/4/23.
 */
public class PeopleTravelByCoach extends HappyPeople {
    protected void travel() {
        System.out.println("Travellingby coach ......");
    }
}

(PeopleTravelByTrain.java)

package com.zhanghf.template.method;

/**
 * Created by Administrator on 2017/4/23.
 */
public class PeopleTravelByTrain extends HappyPeople {
    protected void travel() {
        System.out.println("Travelingby train .....");
    }
}

下面是我们编写测试代码:(Client.java)

package com.zhanghf.template.method;

/**
 * Created by Administrator on 2017/4/23.
 */
public class Client {

    public static void main(String[] args) {
        HappyPeople tom = new PeopleTravelByTrain();
        HappyPeople ross = new PeopleTravelByCoach();
        System.out.println("Tom is going home by train:");
        tom.celebrateSpringFestival();
        System.out.println();
        System.out.println("Ross is going home by coach:");
        ross.celebrateSpringFestival();
    }
}

提出模式

上面的解决方案正是使用了模板方法模式解决了我们的问题:(一个活动需要分多步完成,其中一些步骤是固定不变的,而另一些却不断的变化) 。GOF给出的模板方法模式的定义如下:
Define the skeleton of an algorithm in an operation,deferring somesteps to subclass.Template Method lets subclasses redefine certain steps of analgorithm without changing the algorithm's structure.
翻译:
定义一个操作中的一个算法框架,把一些步骤推迟到子类去实现。模板方法模式让子类不需要改变算法结构而重新定义特定的算法步骤。
也就是说:模板方法定义了一系列算法步骤,子类可以去实现/覆盖其中某些步骤,但不能改变模板方法中这些步骤的执行顺序。
模板方法的好处在于:

  1. 能够解决代码冗余问题.
  2. 把某些算法步骤延迟到子类,子类可以根据不同情况改变/实现这些方法,而子类的新方法不会引父类已有功能的变化。
  3. 易于扩展.
  4. 父类提供了算法的框架,控制方法的执行流程,而子类不能改变算法流程,子类方法的调用由父类模板方法决定。
  5. 父类可以把那些重要的不允许改变的方法屏蔽掉,不让子类覆写(Override/Overwrite)它们。

变化提升

模板方法也有它不足之处,过分地使用模板方法模式,往往会引起子类的泛滥。比如,我们有如下需求:
查询数据库里的记录,一般需要如下步骤完成:

  1. 首先我们需要得到数据库连接
  2. 然后创建statement实例
  3. 并执行相关的查询语句
  4. 最后处理查询出来的结果
  5. 整个执行过程中处理异常。

研究这个步骤,不难发现,整个过程中1、2、3、5逻辑对于每次查询来说,都是相同的。发生变化的部分主要是在对查询结果的处理上。
据上分析,模板方法模式很适合处理这个问题。但是,由于各种各样的查询太多,导致我们需要创建许多子类来处理这些查询,必然引起了子类的泛滥成灾。
为了解决此问题,通常我们结合使用回调来处理这个问题。

回调函数

软件模块之间总是存在一定的接口,从调用方式上可以把他们分为三类:

  1. 同步调用:是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用。
  2. 回调:是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口。
  3. 异步调:用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。

回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础。简图如下:

方法调用.jpg

下面我们就来简单实现一个回调的例子(一):一个笨拙实现---但最能说明回调的例子:
(A.java)

package com.zhanghf.template.method;

/**
 * Created by Administrator on 2017/4/23.
 */
public class A {

    public void call(B b) {
        /*
          在A类中本来是调用 B类的方法,因为传递了一个A的对象。 所以B类对象就可以回头调用A类的方法了
          从而实现了回调(A--调---B--回调---A)
        */
        b.callBack(this);
    }

    //最终调用的方法
    public void printMethod() {
        System.out.println("-------最后回过头来调用A类的方法-------");
    }

    /**
     * Test 代码
     *
     * @param args
     */
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        a.call(b);
    }
}

(B.java)

package com.zhanghf.template.method;

/**
 * Created by Administrator on 2017/4/23.
 */
public class B {
    /**
     * 回调函数
     *
     * @param a
     */
    public void callBack(A a) {
        a.printMethod();
    }
}

通常在面向对象的语言中,回调则是通过接口或抽象类来实现的,我们把实现这种接口的类成为回调类,回调类的对象成为回调对象。

例如:
对上面的代码我们改进如下:(二)
(C.java)------把A类抽象化得到的类

package com.zhanghf.template.method.callback;

/**
 * Created by Administrator on 2017/4/23.
 */
public abstract class C {

    /**
     * 回调函数
     * @param c
     */
    public abstract void callBack(D d);
}

(D.java)

package com.zhanghf.template.method.callback;

/**
 * Created by Administrator on 2017/4/23.
 */
public class D {

    public void call(C c) {
        /*
          在D类中本来是调用 C类的方法,因为传递了一个D的对象。 所以C类对象就可以回头调用D类的方法了。
          从而实现了回调(D--调---C--回调---D)
        */
        c.callBack(this);
    }

    //最终调用的方法
    public void printMethod() {
        System.out.println("-------最后回过头来调用C类的方法-------");
    }

}

(Client.java)类

package com.zhanghf.template.method.callback;

/**
 * Created by Administrator on 2017/4/23.
 */
public class Client {

    public static void main(String[] args) {
        D d = new D();
        d.call(new C() {
            public void callBack(D d) {
                d.printMethod();
            }
        });
    }
}

个人认为,上面的代码(二),虽然实现了回调,但没有清楚地解释回调过程,反而例(一)更能说明回程过程。

最终实现

那么使用回调方式下的模板方法模式会是什么样子的呢。下面把回家过年的那个例子重新实现一下:
步骤如下:
1、 把变化的部化提取出来抽象成接口
(Travel.java)

package com.zhanghf.template.method.callback;

/**
 * Created by Administrator on 2017/4/23.
 */
public interface Travel {
    //坐车
    void travelMethod();
}

2、实现HappyPeople类,这里就不需要抽象了

package com.zhanghf.template.method.callback;

/**
 * Created by Administrator on 2017/4/23.
 */
public class HappyPeople {

    public void celebrateSpringFestival(Travel travel) {
        //买票
        buyTicket();
        //坐车
        travel.travelMethod();
        //团圆
        celebreate();
    }

    //买票
    protected final void buyTicket() {
        System.out.println("Buying ticket .....");
    }

    //团圆
    protected final void celebreate() {
        System.out.println("Happy New Year with family .....");
    }
}

3、无需编写单独具体子类了,而是使用匿名内部类实现

package com.zhanghf.template.method.callback;

/**
 * Created by Administrator on 2017/4/23.
 */
public class TravelByCoachTest {

    public static void main(String[] args) {
        HappyPeople happyPeople = new HappyPeople();
        happyPeople.celebrateSpringFestival(new Travel() {
            public void travelMethod() {
                //TODO Auto-generated method stub
                System.out.println("Travelling by coach.....");
            }
        });
    }

}
package com.zhanghf.template.method.callback;

/**
 * Created by Administrator on 2017/4/23.
 */
public class TravelByTrain {

    public static void main(String[] args) {
        HappyPeople happyPeople = new HappyPeople();
        happyPeople.celebrateSpringFestival(new Travel() {
            public void travelMethod() {
                System.out.println("Travelling by train.....");
            }
        });
    }

}

应用

查询数据库里的记录,变化的部分在于处理查询结果,所以把这步抽象出一个单独的接口。在调用查询时使用内部匿名类去实现。类图如下:
代码实现:
(变化的部分定义成接口)

package com.zhanghf.template.method.search;

import java.sql.ResultSet;

/**
 * Created by Administrator on 2017/4/23.
 */
public interface ResultSetHandler<T> {

    T handle(ResultSet rs);
}

(固定的部分---模板方法部分)

package com.zhanghf.template.method.search;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * Created by Administrator on 2017/4/23.
 */
public class JdbcQueryTemplate {
    public <T> T query(String queryString, ResultSetHandler<T> handler) {
        Connection conn = null;
        PreparedStatement pstm = null;
        ResultSet rst = null;
        try {
            //得到连接
            conn = getConnection();
            pstm = conn.prepareStatement(queryString);
            //执行sql语句,
            rst = pstm.executeQuery();
            //返回结果
            return handler.handle(rst);
        } catch (SQLException e) {
            //处理异常信息
            return null;
        } finally {
            //关闭资源
            closeResource(conn, pstm, rst);
        }
    }

    //得到连接
    public Connection getConnection() {
        return null;
    }

    //关闭资源
    public void closeResource(Connection conn, PreparedStatement pstm, ResultSet rst) {
        //.................
    }
}

代码测试

package com.zhanghf.template.method.search;

import java.sql.ResultSet;

/**
 * Created by Administrator on 2017/4/23.
 */
public class Client {
    public static void main(String[] args) {
        String queryString = "select 'good' from dual";
        JdbcQueryTemplate jdbcTemplate = new JdbcQueryTemplate();
        boolean called = jdbcTemplate.query(queryString, new ResultSetHandler<Boolean>() {
                    public Boolean handle(ResultSet rst) {
                        return Boolean.TRUE;
                    }
                }
        );
        System.out.println(called);
    }
}

到此,我们的第一章(模板方法模式)就讲完了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容