《设计模式》模板方法模式
定义:
模板方法模式又叫模板模式,在一个抽象类中公开定义了执行它的方法的模板,子类可以根据需要重写方法实现,但是调用将按照抽象类中定义的方式进行。
模板方法模式相当于定义了一个操作中算法的骨架,具体的特定步骤的实现延迟到子类中去定义,使得子类可以不更改一个算法的结构,就可以重新定义算法的某些特定步骤。
基本思想就是:算法只存在于父类中,容易修改。如果需要修改算法,只需要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改。
模板方法模式包含的角色:
- 抽象类:负责给出一个算法的轮廓和骨架,它由一个模板方法和若干个基本方法组成。
- 具体子类:实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
作为模板模式的组成角色,抽象类中的模板方法和基本方法又可以细分为:
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
基本方法:定义了实现算法的具体步骤,基本方法又可以分为三种:(1)抽象方法 (2)具体方法 (3)钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般的钩子方法是用于判断的逻辑方法,方法名为isXXX(),返回值类型为 boolean.
模板方法模式的注意事项:
优点:实现了代码的最大化复用,父类的模板方法和已经实现的某些步骤会被子类继承而直接使用。既统一了算法,也提供了很大的灵活性,父类的模板方法确保了算法的结构保持不变,同时子类提供部分步骤的实现。
缺点:**每一个不同的实现都需要一个子类去实现,导致类的个数增加,使得系统更加庞大。**此外,父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,这也提高了代码阅读的难度。
需要注意的是,一般的模板方法需要用 final 修饰,防止子类重写模板方法。
模板方法模式主要用在:算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
模板方法模式类图
案例背景:
经常在家里做饭的同学都知道,炒菜的基本步骤就是:倒油—>热油—>放菜(也指肉)—>放调料—>翻炒等步骤,这应该是大多数炒菜的步骤了,就像一个模板一样,即使是第一次炒菜的同学只要按照这个模板步骤去做,也能大概做个能吃的菜了哈哈!当然,至于味道的合口程序,就看大家自己对菜的口味的把控。这个生活中的炒菜场景,就是模板方法模式的典型应用。
原理类图如下所示:
Cook 类:
public abstract class Cook { // 常量方法,不让子类去实现 public final void cookProcess() { //第一步:倒油 this.pourOil(); //第二步:热油 this.heatOil(); //第三步:放菜 this.pourThings(); //第四步:放调料 this.pourSauce(); //第五步:翻炒 this.fry(); } public void pourOil() { System.out.println("倒油"); } public void heatOil() { System.out.println("热油"); } // 取决于做的是什么菜就放什么东西 public abstract void pourThings(); // 根据不同菜的特点,放入不同的调料 public abstract void pourSauce(); public void fry() { System.out.println("翻炒"); } }
StewedMeat 类:
public class StewedMeat extends Cook{ @Override public void pourThings() { System.out.println("放大肉"); } @Override public void pourSauce() { System.out.println("放酱油、生抽、盐、糖"); } }
FryEggplant 类:
public class FryEggplant extends Cook{ @Override public void pourThings() { System.out.println("放大茄子"); } @Override public void pourSauce() { System.out.println("放酱油、糖、酒、盐、鸡粉、生粉、八角、蒜头"); } }
Client 类:
public class Client { public static void main(String[] args) { // 做红烧肉 StewedMeat stewedMeat = new StewedMeat(); stewedMeat.cookProcess(); System.out.println("-----------------------------"); // 做红烧茄子 FryEggplant fryEggplant = new FryEggplant(); fryEggplant.cookProcess(); } }
如果这时候我想做一个煎豆腐,除了食用油以外,不放任何调料的那种,因此可以省略放调料的步骤。但是,在炒菜的算法中固定了炒菜的步骤,该怎么办呢?此时,我们可以在抽象类中定义一个钩子方法,用于控制放调料的步骤是否需要。
具体的代码如下所示:
public abstract class Cook { public final void cookProcess() { //第一步:倒油 this.pourOil(); //第二步:热油 this.heatOil(); //第三步:放菜 this.pourThings(); if (isOpen()) { //第四步:放调料 this.pourSauce(); } //第五步:翻炒 this.fry(); } public void pourOil() { System.out.println("倒油"); } public void heatOil() { System.out.println("热油"); } // 取决于做的是什么菜就放什么东西 public abstract void pourThings(); // 根据不同菜的特点,放入不同的调料 public abstract void pourSauce(); public void fry() { System.out.println("翻炒"); } // 钩子方法 public abstract boolean isOpen(); }
模板方法模式在 JDK 源码中的应用:
public abstract class InputStream implements Closeable { // 抽象方法,要求子类必须重写 public abstract int read() throws IOException; public int read(byte b[]) throws IOException { return read(b, 0, b.length); } public int read(byte b[], int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } else if (off < 0 || len < 0 || len > b.length - off) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return 0; } int c = read(); // 调用了无参的read方法,该方法是每次读取一个字节数据 if (c == -1) { return -1; } b[off] = (byte)c; int i = 1; try { for (; i < len ; i++) { c = read(); if (c == -1) { break; } b[off + i] = (byte)c; } } catch (IOException ee) { } return i; } }
在 InputStream
父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取 len
个字节数据。而具体读取一个字节数据的细节,由继承 InputStream
的子类去实现。