08、模版方法模式--Template-Method

本节大纲

PS:转载请注明出处
作者: TigerChain
地址: http://www.jianshu.com/p/6c6191a47197
本文出自 TigerChain 简书 人人都会设计模式

教程简介

正文

一、什么是模版方法模式

1、生活中的模版方法模式

1、烧茶、煮咖啡

身为苦逼的程序猿(媛),一定是茶叶和咖啡的忠实粉丝,多少个夜晚加班加点,累了困了喝红牛---不对是喝茶叶、咖啡「我们无形中使用了一个设计模式--模版方法模式」。我们知道不管是烧茶、煮咖啡都基本上分为以下几个步骤:

  • 1、烧水
  • 2、把茶叶或咖啡放入水壶中
  • 3、加热不停的煮
  • 4、把煮好的茶叶或咖啡到入杯子中
  • 5、拿起杯子喝「不能直接喝,小心你的嘴」

我们看到除了原材料放入的不同「茶叶和咖啡」,其它的方法都一毛一样,那么我们把这些方法就可以制定为一个模版「相当于我们有一个既能烧茶又有煮咖啡的器具」,这就是模版定义了一个基本框架

2、高考答题

说了上面的例子,大家可能还懵懵的。那么来说一个更实际的例子,参加过考虑的同学都知道一张考试卷子对所有的同学都是一模一样的,这个卷子就是一个模版,以数学卷子为例吧:有选择题、填空题、判断题、应用题「这都是固定的」--这就是一个题的框架,是一个模版,至于每位考生如何答题那就考生的事情

2、程序中的模版方法模式

模版方法模式的定义

定义一个操作算法的骨架「我们知道这个算法所需要的关键步骤」,而将一些步骤的实现延迟到子类中去实现。通俗的说模版就是一个抽象类,方法就是策略「一些固定的步骤」。模版方法模式是一个行为型模式

模版方法模式的特点

算法的结构不变,子类可以改变模版的某些步骤的实现方式,模版方法就是抽象的封装,一般情况下,模版方法中有一些具体方法「部分逻辑」,抽象方法实现其它剩余的逻辑「子类不同实现的方式就不同」,并且部分逻辑和剩余逻辑共同组成了算法的结构「一般是执行流程,这些流程是固定的,但是具体的实现细节不一样,就可以使用模版方法」

封装不变的部分,扩展可变的部分「可变的部分交给子类去实现」

模版方法模式的目的

模版方法模式的目的就是让子类扩展或者具体实现模版中的固定算法的中的某些算法的步骤

模版方法简单框架

模版方法模式的结构

角色 类别 说明
AbstractClass 抽象类 抽象模版类
ConcreateClass 具体模版 可以有多个「因为每个具体模版实现的内容可能不一样」
HookMethod 钩子方法 不是必须的,是一个开关,用来提供某些方法是否需要调用

模版方法模式简单的 UML

模版方法模式简单的 UML

二、模版方法模式举例

1、把大象装冰箱

把大象装冰箱一共分为几步?我们都知道三步:第一步把冰箱门打开,第二步把大象装进去,第三步把冰箱门盖上。我们把装大象的这三步运作可以看做一个算法的步骤「这个步骤不变」,但是具体的你是使用松下冰箱装大象,还是海尔冰箱装大象,再进一步说使用冰箱装所有动物,大象只是其中的一种,那么就需要抽象出一个模版来,我们使用模版方法模式来实现这一过程

把大象装冰箱简单的 UML

把大象装冰箱简单的 UML

根据 UML 撸码

  • 1、抽象出一个冰箱接口 IRefrige.java
/**
 * Created by TigerChain
 * 抽象冰箱
 */
public interface IRefrige {
    //取得品牌的名字
    String getRefrigeModel() ;
    //设置冰箱品牌
    void setModel(String model) ;
}
  • 2、抽象一个动物类 Animail.java
/**
 * Created by TigerChain
 * 定义动物的抽象类
 */
public abstract class Animal {
    // 取得动物的名字
    abstract String getAnimailName() ;
}
  • 3、定义抽象模版方法类 AbstractMothodWork.java
/**
 * Created by TigerChain
 * 抽象的模版类
 */
public abstract class AbstractMothodWork {
    //打开冰箱
    abstract void open(IRefrige iRefrige) ;
    //把动物装进去
    abstract void putin(Animail animail) ;
    //把冰箱门盖上
    abstract void close() ;

    // 模版方法 定义算法骨架 为了防止子类篡改模版方法步骤,加一个 final
    public final void handle(IRefrige iRefrige,Animail animal){
        this.open(iRefrige); //第一步
        this.putin(animail); //第二步
        this.close();        //第三步
    }
}

我们看到冰箱装动物的步骤是固定的,但是具体步骤内部实现交给子类去处理吧,这就是模版模式的使用场景

  • 4、来一个具体的模版 ConcreateMethodWork.java
/**
 * Created by TigerChain
 * 具体的模版类
 */
public class ConcreateMethodWork extends AbstractMothodWork {

    private IRefrige iRefrige ;
    private Animail animal ;

    @Override
    void open(IRefrige iRefrige) {
        this.iRefrige = iRefrige ;
        System.out.println("第 1 步把 "+iRefrige.getRefrigeModel()+" 门打开");
    }

    @Override
    void putin(Animail animail) {
        this.animail = animal ;
        System.out.println("第 2 步把 "+animail.getAnimailName()+" 装进去");
    }

    @Override
    void close() {
        System.out.println("第 3 步把冰箱门盖上");
    }
}
  • 5、来一个松下冰箱「当然可以是任意品牌的冰箱」 PanasonnicRefrige.java
/**
 * Created by TigerChain
 * 定义一台松下冰箱
 */
public class PanasonnicRefrige implements IRefrige {

    private String model ;
    @Override
    public String getRefrigeModel() {
        return this.model!=null?this.model:"";
    }

    @Override
    public void setModel(String model) {
        this.model = model ;
    }
}
  • 6、被装的对象大象「当然还可以任何动物」 Elephant.java
/**
 * Created by TigerChain
 * 创建一个动物--大象
 */
public class Elephant extends Animal {

    @Override
    String getAnimailName() {
        return "大象";
    }
}
  • 7、测试一下 Test.java
/**
 * Created by TigerChain
 * 测试类
 */
public class Test {
    public static void main(String args[]){
        // 要有冰箱
        IRefrige panasonnicRefrige = new PanasonnicRefrige() ;
        panasonnicRefrige.setModel("松下冰箱");

        // 要有动物,这里是装大象
        Animail elephant = new Elephant() ;

        //来个模版
        AbstractMothodWork  work = new ConcreateMethodWork() ;
        // 执行步骤
        work.handle(panasonnicRefrige,elephant);
    }
}
  • 8、运行查看结果
把大家装冰箱的结果

到此为止,我们就把大象装到冰箱里面了,当然你也可以把老虎、狼、猫装进冰箱「扩展模版即可」,其实我们使用回调也可以实现同样的功能「本质上是模版方法的一种变异---是什么?还是模版方法模式」,我们使用回调方式修改上面代码「不破坏原来的结构,我们直接新加类」

  • 9、我们把想要扩展的方法全部抽象成接口,定义 ITemplate.java
抽象接口
  • 10、定义具体的模版 ConCreateTemplate.java 由于我们把想要扩展的模版方法都抽象出来了,所以我们新建模版的时候就不用抽象了「即定义具体的模版就可以了」
定义具体的模版
  • 11、修改测试类 Test.java 只贴出调用代码
修改测试类

运行结果和 8 中的结果是一样的,这里就不贴图了,具体的代码可以看:https://github.com/tigerchain/designpattern_javademo/tree/master/src/template/putAnimalInRefrigerator

2、数据库增、删、改、查封装

操作过数据库的朋友对数据的增、删、改、查再熟悉不过了,数据库无非就是干这个的。那么我们可以使用模版方法模式把数据库的增、删、改、查封装,至于查什么,改什么,交给具体的模版吧,典型的模版方法模式

数据库增、删、改、查 简单的 UML

数据库增、删、改、查 简单的 UML

根据 UML 撸码

  • 1、定义抽象的模版--数据库增、删、改查抽象类 AbstractDAO.java
/**
 * Created by TigerChain
 * 定义抽象的数据库增、删、改、查的模版
 */
public abstract class AbstractDAO<T> {
    // 增加数据
    abstract void add(T t) ;
    // 根据 id 删除数据
    abstract void delete(int id) ;
    // 更新数据
    abstract void update(T t) ;
    // 根据 id 查找数据
    abstract T findById(int id);
    // 查找所有数据
    abstract List<T> findall() ;
}

我们这里以泛型去接收实体类,至于查那个交给子类去实现--这样就把共同点抽象出来了

  • 2、我们操作用户表吧,定义一个 Person.java
/**
 * Created by TigerChain
 * 定义一个 JavaBean 对应数据库中的表
 */
public class Person {
    private int id ;         // id
    private String name ;    // 姓名
    private int age ;        // 年龄
    private String address ; // 地址
    // 省略 setter 和 getter 方法
    ...   
}
  • 3、定义一个操作用户表的 DAO PersonConCreateDAO.java
**
 * Created by TigerChain
 * 一个具体的模版对用户表的增、删、改、查
 */
public class PersonConCreateDAO extends AbstractDAO<Person> {
    // 库中的用户列表
    private List<Person> persons = new ArrayList<>() ;

    @Override
    void add(Person person) {
        // 实际上应该做插入数据库操作,为了简单我们直接输出语句
        persons.add(person) ;
        System.out.println("添加了 person "+person.toString());
    }

    @Override
    void delete(int id) {
        System.out.println("删除了 id 为 "+id+" person "+persons.get(id-1));
        persons.remove(id-1) ;
    }

    @Override
    void update(Person person) {
        person.setId(1);
        person.setName("TigerChain");
        person.setAge(30);
        person.setAddress("中国陕西西安");

        System.out.println("更新了 person "+person.toString());
    }

    @Override
    Person findById(int id) {
        // 实际这里应该从数据库中查出数据,为了简单模拟一个数据
        Person person = new Person() ;
        if(id ==1){
            person.setId(1);
            person.setName("TigerChain");
            person.setAge(28);
            person.setAddress("中国陕西");
        }
        System.out.println("查找id 为 "+id+" 的 person "+person.toString());
        return person;
    }

    @Override
    List<Person> findall() {
        System.out.println("查找所有的 person "+ persons.toString());
        return persons;
    }
}
  • 4、测试一下 Test.java
public class Test {
    public static void main(String args[]){
        // 模拟两个用户数据
        Person person1 = new Person() ;
        person1.setId(1);
        person1.setName("TigerChain");
        person1.setAge(28);
        person1.setAddress("中国陕西");

        Person person2 = new Person() ;
        person2.setId(2);
        person2.setName("小陈");
        person2.setAge(30);
        person2.setAddress("中国陕西西安");

        PersonConCreateDAO personConCreateDAO = new PersonConCreateDAO() ;

        // 给库中添加用户
        personConCreateDAO.add(person1);
        personConCreateDAO.add(person2);

        // 更新用户 1 的数据
        personConCreateDAO.update(person1);
        personConCreateDAO.findById(1);
        personConCreateDAO.findall() ;

        // 删除一条数据
        personConCreateDAO.delete(1);
        // 查找所有库中的数据
        personConCreateDAO.findall() ;

    }
}
  • 5、运行查看结果
运行结果

至此,我们就使用模版方法模式实现了数据库的增、删、改、查、功能,至于你想操作别的表那直接写一个具体的模版继承抽象模版即可,大家动手写一下,好好的体验一下模版方法模式

3、考试答题

考试卷对每个考生来说都是一样的「考试卷就是一个模版」,至于每个人如何答题那是每个考生的事情,针对考试答题我们可以使用模版方法模式来模拟这一过程,代码就不贴了「我上传到了 github 上」,具体看这里:https://github.com/tigerchain/designpattern_javademo/tree/master/src/template/examination_page

PS:模版方法模式除了抽象模版、具体模版之外,还可能会有一个钩子方法「Hook」,也就是说抽象模版中把规定好了算法的步骤 1 2 3 4 ,如果我只想使用 1 2 3 ,不想使用 4 呢?Hook 方法就派上用场了,以下是抽象模版带 Hook 的伪代码

public abstract class AbstractTemplateMethod{

    abstract void step1() ;
    abstract void step2() ;
    abstract void step3() ;
    abstract void step4() ;
    void step5() ;
    // 模版方法
    public final void execute(){
        
     this.step1() ;
     this.step2() ; 
     this.step3() ;
     if(isUseStep4()){
       this.step4() ;
     }
     this.step5() ;
    }
    // 钩子方法
    protected boolean isUseStep4(){
        return false ;
    }
}

子类重写 isUseStep4() 的方法返回 true 或 fals 决定是否使用 step4 步骤,这就是钩子方法,大家自行感受一下,其实就是一个开关而已

三、Android 源码中的模版方法模式

1、View 中的 draw(Canvas canvas) 方法

我们自定义 View 的时候有时调用 ondraw(Canvas canvas) 方法,这里就用到了模版方法模式,我们来看一下 ondraw 在什么情况下调用「在 View 的 draw() 方法中调用了」,看看 draw() 方法的核心代码

draw 核心代码

这里只不过把抽象方法改成了 protected 的一个空方法而已「本质上是一样的」,具体代理就不贴了,大家动手扒扒这部分源码,其实模版方法模式我们经常用「只不过没有意识到而已」

2、最熟悉的 Activity

Activity 就是一个模版,其中生命周期的方法就是"不固定"的方法,如果要改变子类重写即可

  public class Activity extends ApplicationContext {
      protected void onCreate(Bundle savedInstanceState);
 
      protected void onStart();
 
      protected void onRestart();
 
      protected void onResume();
 
      protected void onPause();
 
      protected void onStop();
 
      protected void onDestroy();
}

这里定义成 protected 方法,那么这个方法既可以是固定的也可以是不固定的「子类实现就不固定,如果不实现就是固定的,很灵活」,Activity 就是一个模版方法模式「你天天使用 Activity 知道它是模版模式吗?」

3、封装 BaseActivity

做过 Android 的朋友肯定都封装过 BaseActivity ,把一些共公的部分抽象出来,然后封装变化,比如我们的 app 应用界面都有共公的头、下面是内容区域,如下图

封装 BaseActivity

然后不同的界面写不同的子类继承即可,我们使用伪代码来模拟一下

public abstract class TemplateMethodActivity extends AppCompatActivity {
    private Button titlebar_btn_left,titlebar_btn_right ;// 左右按钮 
    private TextView titlebar_tv_center ; // 中间文字
    private RelativeLayout content ;      // 内容布局

    private View titleView ;
     @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 加载模版 xml 文件
        setContentView(R.layout.templatemethod_activity);
        initView() ;
        // 设置子类布局
        setContentLayout(getLayoutResID());
        getLayoutResID() ;
        
        init() ;
    }
    // 初始化操作,比如修改按钮样式等
    protected abstract void init();
    // 取得子布局的 xml 文件
    protected abstract int getLayoutResID();
    // 设置到 content 布局上
    private void setContentLayout(int ResId) {
        LayoutInflater.from(this).inflate(ResId, content);
    }
    // 省略若干方法
}

然后子类继承这个 Activity 重写抽象方法即可实现自己的界面内容,我们写一个登录界面继承 TemplateMethodActivity ,代码不贴了,直接上地址:https://github.com/tigerchain/DesignPattern 查看 TemplateMethod 相关代码即可

最终运行效果如下:

模版方法模式实现 BaseActivity 结果

怎么样,是不是一直在使用模版方法模式「只是不知道而已」

四、模版方法模式的优缺点

优点

  • 1、封装不变的部分,扩展可变的部分「交给子类去实现」,这是设计模式一惯的原则「开、闭原则」
  • 2、实现了代码复用

缺点

  • 继承关系本身的缺点,如果父类添加一个新的抽象方法,所有的子类都要改一遍--痛苦

五、模版方法模式 VS 策略模式

以前介绍过策略模式,是对算法的封装,而模版方法模式也是对算法执行,但是它们之间有明显的区别

  • 策略模式:目的是使不同的算法可以被相互替换,不影响客户端的使用

  • 模版方法模式:针对定义一个算法的流程,而将一些不太一样的“具体实现步骤”交给子类去实现「不改变算法的流程」

六、总结

  • 1、抽象类就是一个模版,而接口是一个标准,按这样的规则可以确定该使用接口还是抽象类
  • 2、模版方法模式就是把不固定的步骤实现方式延迟到子类实现的一种方式,它是一种行为模式
  • 3、模版方法模式基本步骤是固定的「实际开发中会有很多变种,比如回调替换,没有固定的步骤全是不固定的等」
  • 4、 一般情况下为了防止子类去更新算法的实现步骤,在抽象的模版方法上加一个 final 关键字

到上为止,模版方法模式就介绍完了,还是那句话,一定要动手试试哦,关注博主,更多精彩内容等着你,手把手教你学会知识点

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