【设计模式_青春版】行为型|模板方法模式

行为型模式

什么是行为型模式。

行为型模式用于描述程序在运行时复杂的流程控制(我们之前学习过很多流程控制语句,例如if else、switch、for循环等等),即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。

什么意思呢?

  • 也就是说如果是多个类的话,那么我们可以使用继承的关系来让其完成复杂的流程控制;

  • 如果是多个对象的话,那么我们可以通过对象的聚合或者组合来完成一个复杂的流程控制。

  • 而这里面必然会涉及到一些算法以及对象之间职责的一个分配,不同的模式,它所涉及的算法以及对象间职责的分配是不一样的。

行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分配行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足"合成复用原则",所以对象行为模式比类行为模式具有更大的灵活性。也即对象行为模式用的更多。

行为型模式分为:

  1. 模板方法模式
  2. 策略模式
  3. 命令模式
  4. 职责链模式
  5. 状态模式
  6. 观察者模式
  7. 中介者模式
  8. 迭代器模式
  9. 访问者模式
  10. 备忘录模式
  11. 解释器模式

注意,对于上面的这11种行为型模式而言,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。

模板方法模式(行为型)

概述

在面向对象程序设计过程中,程序员常常会遇到这种情况:

设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但是某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。

例如,去银行办理业务时一般要经过这4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户都是一样的,可以在父类中实现(因为可以提高代码的复用性),但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。

延迟到子类中实现,就得要在父类中声明抽象方法,对于以上4个流程,它们调用的顺序是固定的,也就是说客户得先取号,然后再去排队,接着办理具体业务,最后对银行工作人员进行评分,因此我们也可以把调用的顺序放在父类中,这就是模板方法模式。

什么是模板方法模式呢?

  • 定义一个操作中的算法骨架(例如,客户去银行办理业务时经过的4个流程,而且调用这4个流程时是有其固定顺序的。
    • 注意,算法骨架是需要在父类中去定义的),而将算法的一些步骤延迟到子类

(例如,上例中对于办理具体业务这一步骤而言,是因人而异的,所以我们就可以把这一步骤的实现推迟到子类中,前提是要在父类中进行抽象声明),使得子类可以在不改变该算法结构的情况下重定义该算法的某些特定步骤。

  1. 我们要先把调用的功能(也可以说成是算法骨架中的步骤)在父类中声明好
  2. 然后子类只需要对里面的某一个步骤或者某些步骤进行一个重新定义(或者重写)就可以了。
  3. 最后,如果我们在子类中要将父类中的流程控制的方法进行重写,那么在父类中的方法不能使用final来修饰

结构

模板方法(Template Method)模式包含以下主要角色:

抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。例如,上例中,取号、排队、办理具体业务、对银行工作人员进行评分这4个流程,它们调用的顺序是固定的,而调用这4个流程的方法,我们就将其称为模板方法,模板方法里面执行的顺序我们可以理解成就是算法的骨架,只不过在这儿基本上没有一些深入的算法。

基本方法:它是实现算法各个步骤的方法,是模板方法的组成部分。

基本方法又可以分为三种:

抽象方法(Abstract Method):一个抽象方法由抽象类声明,并由其具体子类实现,也即要求子类必须重写。

具体方法(Concrete Method):一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。

对于上例而言,对于取号、排队和对银行工作人员进行评分这三个功能来说,它们都是属于具体方法,因为每个人的操作都是一样的,这样,我们就可以把这些功能定义在父类中了,或者就在父类中进行实现,可以提高代码的复用性

而对于办理具体业务这个功能来说,它是因人而异的(每个人办理的业务不一样),所以它就是抽象方法,应该把它推迟到子类中来实现。

钩子方法(Hook Method):在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。也就是说,一种是父类中已经实现了,一种是在父类中没有实现,而是要求子类必须去重写(或者实现)。对于上例来说,它是没有钩子方法的。

对于钩子方法来说,一般它是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型一般都是boolean类型。

具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

在这里插入图片描述

模板方法模式案例

案例模拟炒菜。

分析

炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。

  1. 要定义抽象类以及具体子类的
  2. 而且在抽象类里面,还要分别去定义模板方法和基本方法
  • 模板方法是定义了算法的骨架(对于该案例而言,就是炒菜的步骤,因为炒菜的步骤都是一致的)。在该案例中,模板方法就是烹饪功能,它里面要调用倒油、热油、倒蔬菜、倒调料品、翻炒等这些功能。

那抽象类里面的基本方法又都有哪些呢?

  • 很简单嘛,不就是倒油、热油、倒蔬菜、倒调料品、翻炒等这些方法,这些基本方法里面哪些是抽象方法?

  • 倒油、热油和翻炒这几个方法的具体实现都是一样的,只不过对于炒不同的蔬菜而言,倒的蔬菜以及调料品是不一样的,所以我们就可以将倒蔬菜和倒调料品这俩方法定义成抽象的了,至于其他的三个方法,只须定义成具体方法即可,如此一来,就能提高代码的复用性了。

类图:

在这里插入图片描述

可以看到,顶部有一个抽象类(即父类),它下面又有两个具体的子类,一个是炒包菜类,一个是炒菜心类,它俩都得重写父类中的倒蔬菜和倒调料品这两个方法

代码实现

创建抽象类,该抽象类我们不妨就命名为AbstractClass。

package com.meimeixia.pattern.template;
/**
 * 抽象类(定义模板方法和基本方法)
 */
public abstract class AbstractClass {
    /**
     * 模板方法定义
     *
     * 注意,模板方法是定义了算法的骨架,而子类在继承父类时,是可以对该模板方法进行重写的,
     * 重写的话那就意味着子类可以去改变这个算法的骨架了,但是我们又不能让其去改变,所以我们就只好在该模板
     * 方法上加上final关键字进行修饰了。
     */
    public final void cookProcess() {
        pourOil();
        heatOil();
        pourVegetable();
        pourSauce();
        fry();
    }

    /**
     * 第一步:倒油,属于具体方法。
     *
     * 不管炒啥都是一样的,所以直接实现
     */
    public void pourOil() {
        System.out.println("倒油");
    }

    /**
     * 第二步:热油,属于具体方法。
     *
     * 不管炒啥都是一样的,所以直接实现
     */
    public void heatOil() {
        System.out.println("热油");
    }

    /**
     * 第三步:倒蔬菜,属于抽象方法。
     *
     * 注意了,炒菜时倒的蔬菜是不一样的,例如爆炒包菜时倒的是包菜,爆炒菜心时倒的是菜心
     */
    public abstract void pourVegetable();

    /**
     * 第四步:倒调味料,属于抽象方法。
     *
     * 既然炒菜时倒的蔬菜是不一样的,那么倒的调味料也必然是不一样的了
     */
    public abstract void pourSauce();

    /**
     * 第五步:翻炒,属于具体方法。
     *
     * 不管炒啥都是一样的,所以直接实现
     */
    public void fry() {
        System.out.println("翻炒");
    }
}

创建以上抽象类的子类,让其去重写里面的两个抽象方法。这里我会创建两个子类,一个是炒包菜类,该类我们起名为ConcreteClass_BaoCai。

package com.meimeixia.pattern.template;
/**
 * 炒包菜类
 */
public class ConcreteClass_BaoCai extends AbstractClass {
    @Override
    public void pourVegetable() {
        System.out.println("下锅的蔬菜是包菜");
    }
    @Override
    public void pourSauce() {
        System.out.println("下锅的酱料是辣椒");
    }
}

炒菜心类,该类我们起名为ConcreteClass_CaiXin。

package com.meimeixia.pattern.template;
/**
 * 炒菜心类
 * @author liayun
 * @create 2021-08-02 19:09
 */
public class ConcreteClass_CaiXin extends AbstractClass {
    @Override
    public void pourVegetable() {
        System.out.println("下锅的蔬菜是菜心");
    }
    @Override
    public void pourSauce() {
        System.out.println("下锅的酱料是蒜蓉");
    }
}

创建一个测试类进行测试。

package com.meimeixia.pattern.template;
public class Client {
    public static void main(String[] args) {
        // 现在我们来炒个包菜
        // 创建对象
        ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
        // 调用炒菜的功能
        baoCai.cookProcess();
    }
}

以后如果要想炒其他的蔬菜,那么只须去定义一个子类,让它去继承(或者实现)AbstractClass类即可,而且由于AbstractClass类里面已经定义好了炒菜的步骤,所以子类就不再需要去关注它是如何进行一个翻炒的了,而是只需要去重写它里面的两个抽象方法(分别去指定倒的是什么蔬菜以及倒的是什么调料)就行。

优缺点

优点
  • 提高代码复用性。

在上述案例中,我们是将相同部分的代码放在了抽象的父类中(这样,子类就可以直接去继承使用了,也即提高了代码的复用性),而将不同的代码放入了不同的子类中(注意了,要由父类先声明成抽象的方法,再要求子类必须去重写)。

  • 实现了反向控制。

什么叫实现了反转控制呢?以前我们编写代码时,可能是这样的,在子类中去调用父类的方法,而现在我们是通过一个父类调用其子类的操作,这就是反向控制。

通过一个父类调用其子类的操作,再通过对子类的具体实现扩展不同的行为,就实现了反向控制,这也符合"开闭原则"。后期如果我们想要去添加一些其他的操作的话,那么直接去定义子类就可以了,而不再需要对父类的代码进行修改了,也不再需要对原有的那些子类进行一个修改了。

缺点
  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。

注意了,类的数量增加也是有限的,并不会导致类的个数爆炸式增加,所以这个缺点也还能接受。

  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。

模板方法模式的优点是实现了反向控制,而它的缺点也是反向控制。

为什么会这样说呢?因为如果实现了反向控制的话,那么就会提高代码阅读的难度了。以后,我们在去看别人写的框架的源码的时候,如果里面使用到了模板方法模式这种设计模式,那么我们在看源码时可能就会稍微有一些难以理解了。

使用场景

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。

也就是说,在父类中通过模板方法的形式把算法的整体架构定义出来,至于易变的个别部分(或者个别功能),则在父类中声明成抽象方法,然后要求子类必须去重写。

  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。

这里其实会涉及到钩子函数,也就是说如果有这样的一个场景的话,那么我们就需要定义钩子函数了,只不过上述案例中并没有体现出来,这是因为我们的业务需求里面并不需要用到钩子函数。后期我们在做其他业务时,就可能需要定义钩子函数了。

在JDK源码中的应用

在JDK源码里面,其实很多地方都用到了模板方法模式,我们只看一个类InputStream,它用到的模板方法模式。

在InputStream类中定义了多个read方法,如下所示,很明显它们是重载的。

public abstract class InputStream implements Closeable {
    // 抽象方法,要求子类必须重写
    public abstract int read() throws IOException;

    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
	//模板方法  里面调用了抽象无参方法read()
    public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read(); // 调用了无参的read方法,该方法是每次读取一个字节数据
        if (c == -1) {
            return -1;
        }
        b[off] = (byte)c;

        int i = 1;
        try {
            for (; i < len ; i++) {
                c = read(); // 调用了无参的read方法,该方法是每次读取一个字节数据
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;
    }
}

InputStream类首先是一个抽象类,因为有abstract关键字来修饰它。而且,在该抽象类里面,有三个read方法,第一个是无参的,第二个是带一个参数的,这个参数是字节数组,第三个是带三个参数的。

如果想要一次性去读取多个字节数据,调用带一个参数的read方法!在调用该方法时,它里面又调用了另外一个重载的read方法,即带三个参数的read方法。

那我们接下来就再看看带三个参数的read方法是如何来实现的!你会发现在该方法中又调用了无参的抽象的read方法,既然是抽象的,那么就意味着必须要求子类去重写了,所以在这两处调用无参的抽象的read方法,其本质上调用的是子类中的read方法,这就是反向控制,而反向控制正是模板方法模式的思想。

我们都知道无参的read方法是每次读取一个字节数据,那么读取到之后,**这个字节数据又是如何处理的呢?**其实是将其存储到了一个数组里面。而且,在带三个参数的read方法里面,现在是要读len个字节数据的,所以在该方法里面就用到了for循环,这样就能把每次读到的字节数据全部存储在数组里面了。也就是说,我们一次性读取了多个字节数据,并将其存储在了数组里面。

模板方法模式里面的角色在此处是如何体现的。

InputStream类就是模板方法模式里面的抽象类角色,抽象类里面是有基本方法和模板方法的,**那模板方法究竟是哪个呢?**模板方法就是InputStream类中带三个参数的read方法,它里面定义了算法的骨架。可以看到,在该骨架中,多次调用了无参的抽象的read方法,并且每一次调用该方法都会将获取到的字节数据存储在数组里面。

分析至此,相信大家也知道了InputStream类中的无参的抽象的read方法就是基本方法里面的抽象方法,是要求子类必须去重写的。如果子类去重写的话,那么我们在带三个参数的read方法里面调用的就是子类中重写的方法,这便是反向控制。

以上就是我们对InputStream类源码的一个分析,它里面就用到了模板方法模式。

**总结一下:**在InputStream父类中已经定义好了读取一个字节数组数据的方法,具体实现是每次读取一个字节,并将其存储到数组的第一个索引位置,而且得读取len个字节数据。具体如何来读取一个字节数据则是由子类来实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值