设计模式系列之 模板方法模式

定义

在模板方法中定义一个算法的骨架,而将部分步骤延迟到子类中,使子类在不改变算法结构的情况下,重新定义算法中的部分步骤。

理解该模式的关键就在“模板”二字。什么是模板?模板就是预先定好了结构,后来使用者照葫芦画瓢即可。

通常情况下,算法指用于处理某类任务的确切的指令序列,是对处理逻辑的封装。模板方法模式定义中的骨架同样是对处理逻辑的封装,只是算法中封装的是具体的处理逻辑,即每一步的具体操作。模板方法则封装一个大的步骤,每个步骤中包括若干数量的具体操作,它的封装粒度比算法定义的粒度要稍大。

模式结构

该模式包含模板定义者和模板使用者两类。在java实现中,两者存在继承关系,模板定义者为基类,模板使用者为子类。

模板基类

该类被定义为抽象类,包括模板方法、共有方法、钩子方法、抽象方法。其中

  1. 不变的共有方法
    用于封装全部子类共有的处理逻辑,各个子类稳定的部分。
    在模板基类中,这种方法的处理逻辑已被实现,被final修饰,子类不可修改。
  2. 可变的抽象方法
    用于封装各个子类特有的处理逻辑。
    在模板基类中,这种方法被定义为抽象方法,被abstract修饰,由子类实现具体的方法逻辑。

    这类方法也是模板基类被定义为抽象类的原因。

  3. 钩子方法
    是模板对子类更进一步的开放和扩展,用于控制模板方法是否执行某些步骤,一般返回turefalse。通过判断钩子方法的返回值,来确定是否执行某些步骤。钩子方法是子类和父类通信的通道。
    在模板基类中,这种方法被定义为一般方法,可用protected修饰,子类可以重写。

    通俗地,钩子可分为两部分,拿钩子的一方和被钩的一方。此处拿钩子的是子类 ,被勾的是对可选方法的调用开关。子类使用钩子钩住调用开关,往左钩是调用,往右钩是不调用。

  4. 模板方法
    模板方法模式的核心是模板方法,模板方法实际上是对该模式定义中骨架的实现。模板方法中有序调用若干方法,每一个方法表示一个处理步骤,全部的处理步骤构成一个处理特定业务问题的处理逻辑。
    其中,模板方法内部调用的方法包括上面介绍的三种。需要说明的时,模板方法对以上处理方法的调用顺序是固定的,具备业务含义,是对规范(即骨架)的实现,因此子类不能修改其调用顺序。
    在模板基类中,这种方法的处理逻辑已被实现,被final修饰,子类不可修改。

模板子类

子类继承模板基类,根据业务场景需要,有选择地实现基类中定义的可变方法和钩子方法。

类图

图一 模板方法模式UML图

优缺点

  • 优点
    • 模板方法可以让算法的细节掩盖在子类,同时抽取公共的算法,提高代码复用程度
    • 模板方法可以让修改控制在子类,而父类方法不需要进行改动,符合开放关闭原则。
  • 缺点
    • 类数目增加
    • 继承产生的问题。模板方法类的改动对于所有的算法实现子类都会产生影响,同时模板父类改动违背“开放-关闭”原则
    • 模板方法由于利用钩子控制父类方法,会导致反向控制代码,对于代码的阅读不是十分友好。

代码实践

该业务以制作网课为例,制作网课的流程包含四个步骤

  1. 制作ppt;
  2. 制作视频;
  3. 做笔记 可选项,有些课程做笔记,有些不做;
  4. 提供素材,可变项,不同课程的素材不同;

模板基类

/**
 * @description: 抽象课程
 * @Date: 2021/11/24 11:09
 */
public abstract class ACourse {
    //模板方法
    //规范了处理业务的模板,模板包括
    // 1)不变的公共方法,指各个子类都必须执行的、稳定的部分,被final修饰
    // 2)可选方法,指各子类可选的部分,子类使用钩子方法确定是否在模板中使用这一类方法。
    // 3)变化的方法,指各子类各不相同的部分,子类根据自身业务实现处理逻辑,被abstract修饰。
    final void makeCourse(){
        //不变的方法1-1
        makePPT();
        //不变的方法1-2
        makeVideo();
        if(needWriteNote()){
            //可选方法
            makeNote();
        }
        //可变的方法
        packageCourse();
    }
    //所有子类的公共操作,子类不可修改其处理逻辑。即关闭对 不变的部分 的修改操作
    //但子类可以基于钩子方法决定是否使用该方法
    final void makePPT(){
        System.out.println("制作ppt");
    }
    //所有子类的公共操作,子类不可修改其处理逻辑。即关闭对 不变的部分 的修改操作
    //但子类可以基于钩子方法决定是否使用该方法
    final void makeVideo(){
        System.out.println("制作视频");
    }
    //所有子类的公共操作,子类不可修改其处理逻辑。即关闭对 不变的部分 的修改操作
    //但子类可以基于钩子方法决定是否使用该方法
    final void makeNote(){
        System.out.println("编写笔记");
    }

    /**
     * 钩子方法,用户可以重写,进而决定基类定义的骨架中是否执行某些方法
     * 此处的钩子方法用于控制 是否在模板中执行makeNote方法
     * @return
     */
    protected boolean needWriteNote(){
        return false;
    }
    //基类中变化的部分由子类实现,即对扩展开放
    abstract void packageCourse();
}

三种模板子类

  1. 前端课程
    不做笔记,即使用默认钩子实现。

    /**
     * @description: 前端课程  是ACourse的子类
     * @Date: 2021/11/24 14:19
     */
    public class FrontCourse extends ACourse{
        @Override
        void packageCourse() {
            //前端课程类的处理逻辑
            System.out.println("提供前端课程素材");
        }
    }
    
  2. 设计模式课程
    需要笔记,则需要重写钩子方法

    /**
     * @description: 设计模式课程  是ACourse的子类
     * @Date: 2021/11/24 14:18
     */
    public class DesignPatternCourse extends ACourse{
        @Override
        void packageCourse() {
            //设计模式课程类的处理逻辑
            System.out.println("提供设计模式源码");
        }
    
        /**
         * 重写钩子方法,实现对骨架的控制
         * 此处指在骨架中使用 makeNote方法
         * @return
         */
        @Override
        protected boolean needWriteNote() {
            return true;
        }
    }
    
  3. 运动课程
    在某些情况下,子类无法提前确定模板方法是否调用可选方法,即无法确定钩子方法成立的条件。此时需要将判断条件交给下一层(相对于子类的下一层,即调用子类的位置)调用者。

    /**
     * @description: 运动课程  Acoure子类
     * @Date: 2021/11/24 15:09
     */
    public class SprotCourse extends ACourse {
        private boolean needMakeNote = false;
        @Override
        void packageCourse() {
            System.out.println("体育课");
        }
    
        /**
         * 构造器是调用者同本类通信的窗口,似乎也可称之为钩子方法。
         * @param needMakeNote
         */
        public SprotCourse(boolean needMakeNote) {
            this.needMakeNote = needMakeNote;
        }
    
        /**
         * 由于业务拆分不内聚,基类的子类无法确定使钩子函数成立的条件
         * 则将该条件下方给下一层。此处是运动类课无法确定是否做笔记,则将决定权交给下一级,即调用者。
         * 譬如运动课分为电子竞技运动和球类运动,电子竞技不做笔记,但球类却需要做笔记,无法直接在运动类中确定做不做笔记。
         * @return
         */
    
        @Override
        protected boolean needWriteNote() {
            return this.needMakeNote;
        }
    }
    

适用场景&模式识别

  • 适用场景
    • 一次性实现一个算法的不变部分,并将可变的行为留给子类来实现
    • 各子类中公共的行为被提取出来集中到一个公共父类中,避免代码冗余。
  • 模式识别
    如何识别是否是模板方法 、钩子方法、子类要实现的方法 ,可尝试使用以下三种方式。
    • 观察类图,通过继承关系判断 且父类中有些已实现 一般使用abstract修饰;
    • 钩子方法多用于判断;
    • 父类中定义为抽象方法的方法 需要子类实现

源码应用

  • list arraylist
  • mybatis 中baseExecutor Executor方法

相关知识

  • 抽象类
    抽象类不能用来实例化对象,声明抽象类的唯一目的是为了将来对该类进行扩充。
    如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类。
  • 访问修饰符
    • private 和 public 两极分化,前者只能在类内部访问,后者在所有位置可访问
    • default 包访问权限,只要在一个包下都可以访问,也是默认访问修饰符
    • protected 包和子类可以访问,有特殊情况,略去。
  • 通过重写钩子方法可知,子类重写父类的方法M,那么父类在调用M时,会优先调用子类重写后的M
    • 优点:通过继承相同的父类,初始化子类时,父类会调用不同子类的不同复写方法,从而实现多态性。
    • 缺点:如果在父类构造函数中调用被子类重写的方法,会导致子类重写的方法在子类构造器的所有代码之前执行,从而导致子类重写的方法访问不到子类实例变量的值,因为此时这些变量还没有被初始化。

参考资料

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值