当前位置: 首页 > news >正文

设计模式-模板方法模式

案例分析

咖啡与茶冲泡

咖啡冲泡法

  1. 把水煮沸
  2. 用沸水冲泡咖啡
  3. 把咖啡倒进杯子
  4. 加糖和牛奶

茶冲泡法

  1. 把水煮沸
  2. 用沸水浸泡茶叶
  3. 把茶倒进杯子
  4. 加柠檬

代码实现

public class Coffee{
    //这是咖啡冲泡法,直接取自上面的冲泡步骤
    void prepareRecipe(){
        //以下每个步骤都被实现在分离的方法中
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugarAndMilk();
    }
    
    //每个方法都实现算法中的一个步骤,如煮水,冲泡、倒进杯子,加糖和牛奶
    public void boilWater(){
        System.out.println("Boiling water");
    }
    
    public void brewCoffeeGrinds(){
        System.out.println("Dripping Coffee through filter");
    }
    
    public void pourInCup(){
        System.out.println("Pouring into cup");
    }
    
    public void addSugarAndMilk(){
        System.out.println("Adding Sugar and Milk");
    }
}
public class Tea{
    //与上述咖啡实现很像,其中第2个和第4个步骤不一样,但基本上是相同的冲泡法
    void prepareRecipe(){
        boilWater();
        steepTeaBag();
        pourInCup();
        addLemon();
    }
    //重复代码,与上述冲泡咖啡相同
    public void boilWater(){
        System.out.println("Boiling water");
    }
    
    //冲泡茶专有的方法
    public void steepTeaBag(){
        System.out.println("Steeping the tea");
    }
    //重复代码,与上述冲泡咖啡相同
    public void pourInCup(){
        System.out.println("Pouring into cup");
    }
    //冲泡茶专有的方法
    public void addLemon(){
        System.out.println("Adding Lemon");
    }
}

在实现中,我们发现了重复代码,需要将共同部分抽取出来,放进一个基类中。

抽象类图

image

这个设计如何?是否还忽略了其他共同点?咖啡和茶之间还有什么是相似的?

重新设计

共同点分析

两份冲泡法都采用了相同的算法:

  1. 把水煮沸
  2. 用热水冲泡茶或者咖啡
  3. 把饮料倒进杯子
  4. 在饮料内加入适当的调料

其中步骤1和3已经被抽出来,放到基类中了。但是2和4没有被抽出来,但是他们是一样的,只是应用在不同的饮料上

那么,我们有办法将prepareRecipe()也抽象化吗?

抽象prepareRecipe()

现在,我们从每一个子类中(咖啡或者茶)中逐步抽象prepareRecipe()

  • 抽象第一个问题,就是咖啡使用brewCoffeeGrinds()和addSugarAndMilk()方法,而茶使用steepTeaBag()和addLemon()方法

浸泡(steep)茶和冲泡(brew)咖啡,差异其实不大,我们给一个新的方法名称,比如说brew(),加糖和牛奶和加柠檬也很相似,都是在加入调料,我们也给一个新的方法名称,比如addCondiments(),所以新的prepareRecipe()方法如下:

void prepareRecipe(){
    boilWater();
    brew();
    pourInCup();
    addCondiments();
}
  • 有了新的prepareRecipe()方法,从超类CaffeineBeverage开始
public abstract class CaffeineBeverage{
    //设计为final,因为我们不希望子类覆盖这个方法,我们将步骤2和4抽象为brew()和addCondiments()方法
    final void prepareRecipe(){
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }
    //因为咖啡和茶处理这些方法的做法不同,所以这两个方法声明为抽象,具体交给子类去实现
    abstract void brew();
    abstract void addCondiments();
    
    void boilWater(){
        System.out.println("Boiling water");
    }
    
    void pourInCup(){
        System.out.println("Pouring into cup");
    } 
}
public class Coffee extends CaffeineBeverage{
    @Override
    public void brew(){
        System.out.println("Dripping Coffee through filter");
    }
    
    @Override
    public void addCondiments(){
        System.out.println("Adding Sugar and Milk");
    }
}
public class Tea extends CaffeineBeverage{
    @Override
    public void brew(){
        System.out.println("Steeping the tea");
    }
    
    @Override
    public void addCondiments(){
        System.out.println("Adding Lemon");
    }
}

设计类图

image

认识模板方法

上述实现的就是模板方法模式,来看一下代码结构,它包含了“模板方法”

public abstract class CaffeineBeverage{
    //prepareRecipe()是我们的模板方法,为什么?
    //因为:
    //1.它用作一个算法的模板,在这个例子中,算法是用来制作咖啡因饮料
    //2.在这个模板中,算法内的每一个步骤都被一个方法代表了。
    //3.某些方法是由这个类(也就是超类)处理的
    //4.某些方法则是由子类处理的
    //5.需要由子类提供的方法必须在超类中声明为抽象
    final void prepareRecipe(){
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }
    
    abstract void brew();
    abstract void addCondiments();
    
    void boilWater(){
        //实现
    }
    
    void pourInCup(){
        //实现
    } 
}

模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。

模板方法带我我们什么?

模板方法提供的酷炫咖啡因饮料:

  • 由CaffeineBeverage类主导一切,它拥有算法,而且保护这个算法
  • 对子类来说,CaffeineBeverage类的存在,可以将代码的复用最大化
  • 算法只存在于一个地方,所以容易修改
  • 这个模板方法提供了一个框架,可以让其他的咖啡因饮料插进来,新的饮料只需要实现自己的方法就可以了
  • CaffeineBeverage类专注在算法本身,而由子类提供完整的实现

定义模板方法模式

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

这个模式是用来创建一个算法的模板。什么事模板?模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。

模板方法模式类图

image

对模板方法进行挂钩

abstract class AbstractClass{
    final void templateMethod(){
        primitiveOperation1();
        primitiveOperation2();
        //假设这个方法是新加入的方法,改变了模板方法
        concreateOperation();
        hook();
    }
    
    //定义抽象,由具体的类实现
    abstract void primitiveOperation1();
    abstract void primitiveOperation2();
    //这个具体的方法被定义在抽象类中,将它声明为final,这样一来子类就无法覆盖它。它可以被模板方法直接使用,或者被子类使用。
    final void concreateOperation(){
        //这里是实现
    }
    //我们也可以有“默认不做事的方法”,我们称这种方法为“hook”(钩子)。子类可以视情况决定要不要覆盖它们。
    void hook(){}
}

钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。

我们来看一下钩子的用途:

public abstract class CaffeineBeverageWithHook{
    void prepareRecipe(){
        boilWater();
        brew();
        pourInCup();
        //加上一个小的条件语句,该条件是否成立,是由一个具体方法customerWantsCondiments()决定的,如果顾客“想要”调料,只有这时我们才调用addCondiments()
        if(customerWantsCondiments()){
            addCondiments();
        }
    }
    
    abstract void brew();
    abstract void addCondiments();
    
    void boilWater(){
        System.out.println("Boiling water");
    }
    
    void pourInCup(){
        System.out.println("Pouring into cup");
    }
    
    //我们在这里定义了一个方法,(通常)是空的缺省实现。这个方法只会返回true,不做别的事。
    //这就是一个钩子,子类可以覆盖这个方法,但不见得一定这么做。
    boolean customerWantsCondiments(){
        return true;
    }
}
public class CoffeeWithHook extends CaffeineBeverageWithHook{

    public void brew(){
        System.out.println("Dripping Coffee through filter");
    }
    
    public void addCondiments(){
        System.out.println("Adding Sugar and Milk");
    }
    
    //覆盖钩子,提供了自己的功能
    @Override
    public boolean customerWantsCondiments(){
        //让用户输入他们对调料的决定,根据用户的输入返回true或者false
        String answer = getUserInput();
        
        if(ansser.toLowerCase().startsWith("y")){
            return true;
        }else{
            return false;
        }
    }
    
    private String getUserInput(){
        String answer = null;
        
        System.out.println("Would you like milk and sugar with your coffee (y/n)?");
        
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        
        try{
            answer = in.readLine();
        }catch(IOException ioe){
            System.error.println("System.in");
        }
        
        if(answer == null){
            return "no";
        }
        
        return answer;
    }
}

模板方法模式使用场景

当我创建一个模板方法时,怎么才能知道什么时候该使用抽象方法,什么时候使用钩子呢?

  • 当你的子类“必须”提供算法中某个方法或步骤的实现时,就使用抽象方法
  • 如果算法的这个部分是可选的,就使用钩子
  • 如果是钩子的话,子类可以选择实现这个钩子,但并不强制这么做

模板方法模式应用

模板方法排序

java数组类的设计者提供给我们一个方便的模板方法用来排序。

  • mergeSort方法包含排序算法,此算法依赖于compareTo()方法的实现来完成算法,可以把该方法想象成一个模板方法。
  • 我们需要实现compareTo()方法,“填补”模板方法的缺憾。
  • sort()方法希望能够用于所有的数组,所以他们定义了一个静态方法,而由被排序的对象内的每个元素自行提供比较大小的算法部分。
  • 由于不需要继承数组就可以使用这个算法,这样使得排序变得更有弹性、更有用
  • 虽然这不是教科书的模板方法,但它的实现仍然符合模板方法模式的精神

总结

  1. “模板方法”定义了算法的步骤,把这些步骤的实现延迟到子类
  2. 模板方法模式为我们提供了一种代码复用的重要技巧
  3. 模板方法的抽象类可以定义具体方法、抽象方法和钩子
  4. 抽象方法由子类实现
  5. 钩子是一种方法,它在抽象类中不做事,或者只做默认的事情,子类可以选择要不要去覆盖它
  6. 为了防止子类改变模板方法中的算法,可以将模板方法声明为final
  7. 好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用低层模块
  8. 策略模式和模板方法模式都封装算法,一个用组合,一个用继承
  9. 工厂方法是模板方法的一种特殊版本

相关文章:

  • MySQL数据库01——mysql的安装和配置(包含workbench安装,超详细)
  • 2/4考试总结
  • Ta-Lib源码解析(三):蜡烛图指标 (Candlestick Indicator) #(附Python重构代码和示意图)(补充中)
  • 【嵌入式】MDK使用sct文件将代码段放入RAM中执行
  • 《基于Xilinx的时序分析、约束和收敛》目录与传送门
  • Jackson序列化带有注解的字段的原理浅析
  • 第十届“图灵杯”NEUQ-ACM程序设计竞赛题解(A-I)
  • Linux ALSA 之九:ALSA ASOC Codec Driver
  • 一文读懂JVM类加载机制过程及原理
  • 一文带你入门MyBatis Plus
  • java split()方法 toLowerCase() 方法
  • Ubuntu - command checklist
  • 小程序-模板与配置-WXSS模板样式
  • 小程序-模板与配置-WXML模板语法
  • 群晖NAS安装frp实现内网穿透(非Docker)
  • Linux性能学习(2.1):内存_查看系统内存以及Buffer Cached说明
  • Connext DDS开发指南(5)基本QoS策略
  • MoCoViT: Mobile Convolutional Vision Transformer
  • Harbor安装
  • leetcode解题思路分析(一百三十六)1158 - 1169 题
  • 电加热油锅炉工作原理_电加热导油
  • 大型电蒸汽锅炉_工业电阻炉
  • 燃气蒸汽锅炉的分类_大连生物质蒸汽锅炉
  • 天津市维修锅炉_锅炉汽化处理方法
  • 蒸汽汽锅炉厂家_延安锅炉厂家
  • 山西热水锅炉厂家_酒店热水 锅炉
  • 蒸汽锅炉生产厂家_燃油蒸汽发生器
  • 燃煤锅炉烧热水_张家口 淘汰取缔燃煤锅炉
  • 生物质锅炉_炉
  • 锅炉天然气_天燃气热风炉