关注「Java视界」公众号,获取更多技术干货

模板模式 (Template Method Pattern)

一、什么是模板

        既然是模板设计思维,那首先要探索一下到底何为模板,搞清楚现实中模板是啥样的,才能进一步把模板抽象化。

        一提到模板,我最先想到的是PowerPoint,没错,就是让人头疼的PPT,马上要述职汇报了,PPT确实让人头大~

人事的小姐姐已经把汇报的ppt模板发给我们了,我看了下汇报流程,如下:

不管你是张三、李四还是王五,都要按照这4个方面按顺序进行汇报,不能别出心裁搞个性化,这个就是模板的作用。 

百度百科给了“模板”一个定义:“模板是将一个事物的结构规律予以固定化、标准化的成果,它体现的是结构形式的标准化。

二、现实生活中的模板

勤劳的小伙伴都做过饭吧?一起炒个菜吧!

以上,炒茄子和炒藕带都可以大致分成四步,其中第一和第四步都是一样的,第二步因为炒的菜不同,第三步根据口味放的调料不同,但无论是炒啥菜都基本遵循以上四步,只是个别步骤内容不同,这样我们就可以把上面的步骤抽象成模板,如下:

三、模板的代码实现

上面的模板是现实世界的抽象出来的,如何在代码的世界实现呢?

一般需要一个基类及其子类进行实现,基类一般是一个抽象类,为所有子类提供算法框架;子类则根据自身需求提供具体的实现。上面的可以炒XXX的模板可以理解为抽象基类,炒茄子或炒藕带就是具体子类。

代码实现如下:

抽象基类

/**
 * 抽象基类,为所有子类提供算法框架
 * (为什么是抽象类而不是接口? 因为抽象类内允许实现一些共有或通用的方法,这些方法就
 *  无需每个子类再去实现了,如上面的”准备炒锅“和“装盘”是通用无差别的方法,可以直接在
 *  基类实现;而接口内都为抽象方法,不能实现一些通用的方法)
 */
public abstract class Cooking {

    /** 炒菜的步骤模板方法  final 方法可以被继承和重载,但不能被重写,模板方法流程是固定的 */
    public final void doCooking(){
        // 1.准备炒锅
        preparePot();
        // 2.开始炒菜
        startFry();
        // 3.放调料
        addCondiments();
        // 4.装盘
        panning();
    }

    /** 1.准备炒锅(该方法为通用无差别方法,直接在基类里实现,private则子类不可见) */
    private void preparePot(){
        System.out.println("准备炒锅");
    }

    /**
     * 2.开始炒菜
     * (该方法为个性化方法,延迟到子类里实现,定义为抽象方法
     *   因此不能用private修饰,用该为protected或default)
     */
    protected abstract void startFry();

    /**
     * 3.放调料
     * (该方法为个性化方法,延迟到子类里实现,定义为抽象方法
     *   因此不能用private修饰,用该为protected或default)
     */
    protected abstract void addCondiments();

    /** 4.装盘(该方法为通用无差别方法,直接在基类里实现,private则子类不可见) */
    private void panning(){
        System.out.println("装盘");
    }
}

炒茄子子类

public class Eggplant extends Cooking{

    @Override
    protected void startFry() {
        System.out.println("开始炒茄子");
    }

    @Override
    protected void addCondiments() {
        System.out.println("放盐");
    }
}

炒藕带子类:

/**
 * 子类:炒藕带
 */
public class Lotus extends Cooking{

    @Override
    protected void startFry() {
        System.out.println("开始炒藕带");
    }

    @Override
    protected void addCondiments() {
        System.out.println("放醋");
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        Cooking cookEggplant = new Eggplant();
        Cooking cookLotus = new Lotus();
        cookEggplant.doCooking();
        System.out.println("-----------");
        cookLotus.doCooking();
    }
}

结果:

准备炒锅
开始炒茄子
放盐
装盘
-----------
准备炒锅
开始炒藕带
放醋
装盘

ps:这里延迟到子类里面实现的方法都是用protect修饰的,为什么?

一方面,这种方法只有子类才能实现,其他外部类不能实现,protect修饰可以满足要求;

另一方面,这里不用public是因为模板类里面的方法组成一个整体流程,给外面的类实现其中一个也没有意义。

四、钩子方法

以上实现了模板方法,子类根据需要继承并实现基类的抽象方法,但是这里是实现全部的抽象方法,如果现在有个子类不想实现“装盘”方法,炒好了就想直接在锅里吃,怎么办呢?

这时候需要提供一个钩子方法,提供一个默认或者空的实现,此钩子方法可以作为子类是否实现基类某个方法的判断依据。

例如我们在基类添加一个钩子方法如下:

/**
 * 抽象基类,为为所有子类提供算法框架
 * (为什么是抽象类而不是接口? 因为抽象类内允许实现一些共有或通用的方法,这些方法就
 *  无需每个子类再去实现了,如上面的”准备炒锅“和“装盘”是通用无差别的方法,可以直接在
 *  基类实现;而接口内都为抽象方法,不能实现一些通用的方法)
 */
public abstract class Cooking {

    /** 炒菜的步骤模板方法 */
    public final void doCooking(){
        // 1.准备炒锅
        preparePot();
        // 2.开始炒菜
        startFry();
        // 3.放调料
        addCondiments();
        // 4.装盘
        if(willPan()){
            panning();
        }

    }

    /** 1.准备炒锅(该方法为通用无差别方法,直接在基类里实现,private则子类不可见) */
    private void preparePot(){
        System.out.println("准备炒锅");
    }

    /**
     * 2.开始炒菜
     * (该方法为个性化方法,延迟到子类里实现,定义为抽象方法
     *   因此不能用private修饰,用该为protected或default)
     */
    protected abstract void startFry();

    /**
     * 3.放调料
     * (该方法为个性化方法,延迟到子类里实现,定义为抽象方法
     *   因此不能用private修饰,用该为protected或default)
     */
    protected abstract void addCondiments();

    /** 4.装盘(该方法为通用无差别方法,直接在基类里实现,private则子类不可见) */
    private void panning(){
        System.out.println("装盘");
    }

    /** 钩子方法 (为另一个方法提供默认或空的实现, protected让子类可以覆写)*/
    protected boolean willPan(){
        return true;
    }
}

子类覆写钩子方法:

/**
 * 子类:炒茄子
 */
public class Eggplant extends Cooking{

    @Override
    protected void startFry() {
        System.out.println("开始炒茄子");
    }

    @Override
    protected void addCondiments() {
        System.out.println("放盐");
    }

    /** 覆写基类的钩子方法 (不“装盘”)*/
    @Override
    protected boolean willPan(){
        return false;
    }
}

测试类Test:

public class Test {
    public static void main(String[] args) {
        Cooking cookEggplant = new Eggplant();
        Cooking cookLotus = new Lotus();
        cookEggplant.doCooking();
        System.out.println("-----------");
        cookLotus.doCooking();
    }
}

结果:

准备炒锅
开始炒茄子
放盐
-----------
准备炒锅
开始炒藕带
放醋
装盘

炒茄子已经不装盘了。可以看出,钩子方法使子类自由度提高了,想不实现哪个流程就不用实现,但总体还是按照模板进行的

五、优缺点

以上过程以不难看出模板设计模式的优点

优点:

  • 封装性好
  • 复用性好
  • 屏蔽细节
  • 便于维护

缺点:

由于java的单继承性,一旦一个类采用了模板设计模式,就意味着它需要继承基类,也同时意味着它不能再继承另一个类。

 每个不同的实现都需要定义一个子类,会导致类的个数增加。

六、适用的场景

  • 算法或操作遵循相似的逻辑或流程
  • 把相同的代码抽取到父类中时
  • 当算法很复杂时,可以利用模板方法将算法进行分解

七、总结

模板设计的现实需要两个要素:抽象基类和子类

抽象基类中,将部分通用无差别的逻辑以具体方法实现;另外个性化差异的部分声明成抽象方法,在其子类中延迟实现;钩子方法增加子类的灵活性;最后将所有方法汇总成一个finnal修饰的模板方法,调用该方法即可完成整个流程。

posted @ 2022-06-25 14:03  沙滩de流沙  阅读(115)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货