设计模式之模板方法模式

2020-08-17   2,321 次阅读


初步认识模板方法模式

诞生缘由

很多对象的行为步骤相似且固定,只是不同对象完成某些步骤的方式有些差异。不想为每个对象都写一套相同的行为步骤,
又想去适应不同对象在执行某一个步骤的差异化,模板方法模式就诞生了。

这样的情况在我们的生活中也是经常出现,比如学校的考试,不管是哪个科目的考试,大致都是这几个步骤:学校发出通知,老师准备试卷,学生报名,学生参加考试,老师阅卷出成绩。
不同的科目,出卷老师不一样,考卷内容不一样,考试时间不一样,阅卷老师不一样等等,但是整个流程大致都一样的。

在“模板方法模式”中,以上的这种具有一系列相似步骤的这个行为,我们称之为“算法”。下面给出模板方法模式的定义:

定义

在抽象超类中定义一个算法骨架,骨架中的具体执行步骤由子类去实现。

通关文牒

现在对模板方法模式有一些初步的认识了,那就带上通关文牒当一次唐僧吧!不取经不罢休,要做一个知其然而知其所以然的“佛”。本次关卡如下:

  • 如何定义抽象超类中的算法骨架?

背景

夏天到了,A公司的打算在自己的游戏中新增两种饮料供用户使用,以此来提高游戏的趣味性,它们分别是“冲浪冰咖啡”与“银山蜂蜜茶”。
这对于小媛来说,那可简单了,三分钟不到,两种饮料的制作过程就出炉了:

/**
 *  冲浪冰咖啡
 */
public class SurfIcedCoffee {
    public void prepareCoffee(){
        System.out.println("准备咖啡");
    }

    public void putInWesternCup(){
        System.out.println("放入西式杯具");
    }

    public void withIce(){
        System.out.println("加冰");
    }

    public void drawing(){
        System.out.println("在表层画冲浪图");
    }
    
    // 制作饮料
    public void make(){
       prepareCoffee(); 
       putInWesternCup();
       withIce();
       drawing();
    }
}
/**
 * 银山蜂蜜茶
 */
public class YinshanHoneyTea {
    public void prepareTea(){
        System.out.println("准备茶");
    }

    public void putInChineseCup(){
        System.out.println("放入中式杯具");
    }

    public void withHoney(){
        System.out.println("加蜂蜜");
    }

    public void drawing(){
        System.out.println("在表层画银山图");
    }
    
    // 制作饮料
    public void make(){
       prepareCoffee(); 
       putInChineseCup();
       withIce();
       drawing();
    }
}

以上的代码发现了吗?两种饮料制作工艺都差不多,这种极其相似的制作流程我们是否可以抽象封装一下呢?

增加需求

还没等小媛考虑清楚,公司的新任务下来了,决定再加一种饮料,为新上市的杯子打广告,该饮料的名称是“啵啵草莓牛奶”。小媛思前想后,还是决定抽象一个超类出来,
一下是超类的代码:

/**
 *  饮料的父类
 */
public abstract class Drinks {
    // 准备主料,不同的饮料需要的主料不一样,由子类自行实现
    public abstract void prepareMainMaterial();
    
    // 放入杯具,不同的饮料使用的杯具不一样,由子类自行实现
    public abstract void putInCup();

    // 加调味品,不同的饮料添加的调味品不一样,由子类自行实现
    public abstract void withCondiment();

    // 画图案,不同的饮料在表层描绘的图案不一样,由子类自行实现
    public abstract void drawing();
    
    // 制作饮料
    public void make(){
        prepareMainMaterial();
        putInCup();
        withCondiment();
        drawing();
    }
}

饮料类中的make方法被提取到了抽象父类中,子类继承它后,只需要做各自差异的部分即可,相同的骨架部分已经由父类去完成了,以下是3种饮料的代码:

/**
 *  冲浪冰咖啡
 */
public class SurfIcedCoffee extends Drinks {
    @Override
    public void prepareMainMaterial(){
        System.out.println("准备咖啡");
    }
    
    @Override
    public void putInCup(){
        System.out.println("放入西式杯具");
    }
    
    @Override
    public void withCondiment(){
        System.out.println("加冰");
    }
    
    @Override
    public void drawing(){
        System.out.println("在表层画冲浪图");
    }
}
/**
 * 银山蜂蜜茶
 */
public class YinshanHoneyTea  extends Drinks {
    @Override
    public void prepareMainMaterial(){
        System.out.println("准备茶");
    }
    
    @Override
    public void putInCup(){
        System.out.println("放入中式杯具");
    }
    
    @Override
    public void withCondiment(){
        System.out.println("加蜂蜜");
    }
    
    @Override
    public void drawing(){
        System.out.println("在表层画银山图");
    }
}
/**
 * 啵啵草莓牛奶
 */
public class BoboStrawberryMilk  extends Drinks {
    @Override
    public void prepareMainMaterial(){
        System.out.println("准备牛奶");
    }
    
    @Override
    public void putInCup(){
        System.out.println("放入公司新上市的杯子中");
    }
    
    @Override
    public void withCondiment(){
        System.out.println("加草莓");
    }
    
    @Override
    public void drawing(){
        System.out.println("在表层画啵啵图");
    }
}

好了,现在我们对算法骨架进行了抽象,以后新增饮料,只需要继承父类,完成自己变化的那部分代码就行了,哪天公司想为某一个旅游
景点打广告,要在饮料上描绘该景点的宣传照也很好处理。

总结

案例解析

本例中饮料的制作流程,我们可以理解为一个算法实现的基本步骤,将其封装后,任何具有相似制作工艺的对象(饮料)都可以继承Drinks
这个类,然后实现自己变化的那一部分,这就是模板方法模式,抽象算法骨架(步骤),具体实现交给子类去完成。

适用场景

“模板方法模式”适用于对象的一个操作的实现具有一系列相似的步骤,这时候需要将这个行为抽离出来进行封装,在父类中完成骨架的部分,让它的子类去实现会发生
变化的部分,这样的方法定义为抽象方法。
比如各个饭馆的就餐流程,不管饭馆是火锅店还是中餐店或是西餐店,整个就餐流程大致都是“列出菜单”,“用户勾选菜品”,
“厨房准备菜肴”,“服务员上菜”等一系列相似的行为,不同的是不同类型的饭馆菜品会存在差异。

优缺点

  • 优点:将不变的骨架部分抽离到父类中,提高了代码的重用性;子类实现变化的部分,方便后期的扩展,模板方法模式对新增开放,对修改关闭,符合开闭原则。
  • 缺点:模板方法模式的步骤流程一旦封装到父类中,就不可变更,如果后期要更换骨架的部分,就需要改动父类的代码,影响较大。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议