15-模板方法模式

一、什么是模板方法设计模式

从字面意义上理解, 模板方法就是定义出来一套方法, 作为模板, 也就是基础。 在这个基础上, 我们可以进行加工,实现个性化的实现。比如:一日餐三. 早餐, 中餐, 晚餐. 每个人都要吃三餐, 但每个人的三餐吃的可能都不一样. 一日三餐定义了模板–早中晚, 每个人的三餐就是模板的具体实现.

1. 模板方法的用途

  • 将不变的行为从子类搬到超类,去除了子类中的重复代码。
  • 规范子类的结构

2. 模板方法的定义

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式

二、定义模板方法的步骤

第一步: 定义模板类

第二步: 定义具体子类

第三步: 客户端调用

下面来了解每一个步骤:

1. 定义模板类

通常模板类是抽象类,负责给出算法的轮廓或者框架。他是有若干个模板方法和若干个基本方法构成。

1.1 模板方法

定义了算法的骨架, 定义了方法调用的顺序, 其中包含一个或者多个基本方法

1.2 基本方法

基本方法有三种类型:

  • 抽象方法:子类必须重写的方法。没有默认实现。
  • 具体方法:父类定义的默认实现,有实现逻辑,可以被具体的子类继承或重写
  • 钩子方法:判断的逻辑方法和需要子类重写的空方法两种。

2. 定义具体子类

具体子类,也就是具体的实现类, 实现抽象类中的抽象方法。他们是抽象的模板方法中一个组成部分。

3. 定义客户端调用

客户端调用抽象类, 实例化的时候实例化具体类, 只需要调用抽象类的模板方法就可以了。

4. 实例演示

下面来看一下抽象类和子类之间的UML图和源码实现

UML类图

模板方法设计模式

从图中可以看出抽象类的结构可以定义三类方法。 可以有一个也可以有多个。子类必须需要实现抽象类中的抽象方法,可以选择性重写父类的具体方法。子类实现接口的时候,要多思考设计模式的六大原则。

代码实现

  • 先定义抽象类, 也就是框架。

    /**
     * 抽象类, 定义模板
     */
    public abstract class AbstractClass {
    
        /**
         * 模板方法
         * 规范了流程的框架
         */
        public void templateMethod() {
            // 先调用具体方法
            specificMethod();
            // 在调用抽象方法
            abstractMethod();
        }
    
        public void specificMethod() {
            // 具体的公共逻辑, 父子类通用
            System.out.println("具体方法---父子类通用逻辑");
        }
    
        /**
         * 抽象方法
         * 抽象方法, 子类必须重写
         */
        public abstract void abstractMethod();
    }
  • 定义具体的实现类, 实现父类的抽象方法

    /**
     * 具体实现类
     */
    public class ConcreteClass extends AbstractClass {
    
        /**
         * 重写父类的抽象方法
         */
        @Override
        public void abstractMethod() {
            System.out.println("具体实现类--重写父类的抽象方法");
        }
    }
  • 最后定义客户端调用

    /**
     * 模板方法客户端
     */
    public class TemplateClient {
        public static void main(String[] args) {
            AbstractClass abstractClass = new ConcreteClass();
            abstractClass.templateMethod();
        }
    }

    运行结果:

    具体方法---父子类通用逻辑
    具体实现类--重写父类的抽象方法

对照模板方法设计模式,我们来看一个具体的案例。

三、案例解析

1. 案例1: 一日规划

每个人的一日安排都有三餐, 早餐, 中餐,晚参。 但每个人的三餐食物不尽相同,我们来看看每个人的三餐变化, 以及三餐前后要做的事情。

/**
 * 一日三餐抽象类
 */
public abstract class ArrangementAbstract {
    /**
     * 模板方法
     * 规定了一天的框架
     */
    public void templateMethod() {
        System.out.println("一日安排如下: ");
        getUp();
        breakfast();
        lunch();
        dinner();
        getDown();
    }

    public void getUp() {
        System.out.println("起床");
    }

    public void getDown() {
        System.out.println("睡觉");
    }

    /**
     * 早餐抽象类
     */
    public abstract void breakfast();

    /**
     * 午餐抽象类
     */
    public abstract void lunch();

    /**
     * 晚餐抽象类
     */
    public abstract void dinner();
}

定义一日三餐抽象类。每个人的日程安排都是,起床,早餐,中餐,晚餐,睡觉。 其中起床和睡觉是每个人都要做的事情,三餐也是,但三餐的食物不同,于是我们将三餐定义为抽象

一日安排实现类

/**
 * 张三的一日三餐安排
 */
public class PersonArrangement extends ArrangementAbstract {
    private String name;

    public PersonArrangement(String name) {
        this.name = name;
    }

    /**
     * 早餐抽象类
     */
    public void breakfast() {
        System.out.println(name + "--早餐吃牛奶面包");
    }

    /**
     * 午餐抽象类
     */
    public void lunch() {
        System.out.println(name + "--中餐吃食堂");
    }

    /**
     * 晚餐抽象类
     */
    public void dinner() {
        System.out.println(name + "--晚餐吃水果");
    }
}

客户端调用

/**
 * 测试类
 */
public class Client {
    public static void main(String[] args) {
        ArrangementAbstract arrangementAbstract = new PersonArrangement("张三");
        arrangementAbstract.templateMethod();
    }
}

运行结果:

一日安排如下: 
起床
张三--早餐吃牛奶面包
张三--中餐吃食堂
张三--晚餐吃水果
睡觉

可以看出, 完全按照模板方法的步骤实现。

2. 案例2: 钩子方法

我们上面说了, 模板方法设计模式中, 基本方法包括抽象方法,具体方法和钩子方法. 如果能够使用好钩子方法, 可以在程序中完美实现子类控制父类的行为. 我们来看下面的案例:

我们在抽象方法中定义一个钩子方法 hookMethod() , 在模板方法 templateMethod() 中,钩子方法控制了代码的流程.

UML类图

模板方法模式-钩子方法

代码实现如下:

/**
 * 抽象类, 定义模板
 */
public abstract class AbstractClass {

    /**
     * 定义模板方法
     * 规范了流程的框架
     */
    public void templateMethod() {
        // 调用具体方法
        specificMethod();
        // 钩子方法控制下一步骤
        if (hookMethod()) {
            // 调用抽象方法
            abstractMethod();
        }
    }

    /**
     * 具体方法
     */
    public void specificMethod() {
        // 具体的公共逻辑, 父子类通用
        System.out.println("具体方法---父子类通用逻辑");
    }

    /**
     * 抽象方法
     *
     * 抽象方法, 子类必须重写
     */
    public abstract void abstractMethod();

    /**
     * 钩子方法
     * 钩子方法是有具体实现的,
     */
    public boolean hookMethod() {
        return true;
    }
}


/**
 * 具体实现类
 */
public class ConcreteClass extends AbstractClass{

    /**
     * 重写父类的抽象方法
     */
    @Override
    public void abstractMethod() {
        System.out.println("具体实现类--重写父类的抽象方法");
    }

    /**
     * 子类重写钩子方法,非必须
     * @return
     */
    @Override
    public boolean hookMethod() {
        System.out.println("重写了父类的钩子方法, 反向控制父类的行为");
        return false;
    }
}

/**
 * 测试类
 */
public class TemplateClient {
    public static void main(String[] args) {
        AbstractClass abstractClass = new ConcreteClass();
        abstractClass.templateMethod();
    }
}

运行结果

具体方法---父子类通用逻辑
重写了父类的钩子方法, 反向控制父类的行为

如果子类钩子方法 HookMethod() 的代码改变,则程序的运行结果也会发生改变。

四、模板方法的优缺点

优点

  • 规范了框架, 封装了不变的部分, 扩展了可变的部分. 父类定义框架, 并抽象了公共不变的部分, 子类通过重写扩展完善了框架的实现.
  • 使用了”开闭原则”, 对扩展开放, 对修改关闭. 子类可以通过重写父类的抽象方法来扩展父类的实现.
  • 行为集中有父类控制, 规范流程。

缺点

  • 每一种实现都需要定义一个具体实现类, 增加类的数量, 系统更加复杂
  • 继承的缺点, 一旦父类增加一个抽象方法, 所有子类都需要增加. 这一点违背”开闭原则”.
  • 父类中的抽象方法由子类实现, 子类的执行结果影响父类, 这种”反向控制”结构, 会增加代码的复杂性。

五、使用场景

  • 算法的整体步骤是固定的,但个别部分容易发生变化时,可以考虑使用模板方法设计模式,将容易发生变化的部分抽象出来,提供给子类去实现。
  • 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  • 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
  • 重构时,模板方法模式是一个经常使用到的模式,把相同的代码抽取到父类中,通过钩子函数约束其行为

六、对设计模式六大原则的应用思考

  • 单一职责原则: 一个方法只有一个引起变化的原因, 这个不太好看出, 要看子类代码的具体实现
  • 里式替换原则: 父类出现的地方都可以使用子类替换,并且结果保持一致. 子类重写了父类的方法。 模板方法设计模式可能违背里式替换原则, 不过,这正是能够“反向控制”的原理
  • 接口隔离原则: 依赖于最小的单一接口, 而不是胖接口. 符合
  • 依赖倒置原则: 依赖于抽象, 而不是依赖于具体. 符合
  • 迪米特法则: 最少知识原则. 之和朋友沟通, 减少和朋友的沟通. 这个需要看子类具体实现是否符合
  • 开闭原则: 违背开闭原则, 一旦父类增加一个抽象方法, 所有子类都需要对应增加

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 george_95@126.com