介绍
基本介绍
模板方法模式
,又叫模板模式
,在一个抽象类中定义了一个执行它的其他方法的公开模板方法,子类可以按需重写抽象类的抽象方法- 简单说,模板方法模式 定义一个操作中的算法(或者说流程)的骨架,而将一些步骤下放到子类中实现,使得子类可以在不改变算法结构的基础上,可以重新定义算法的某些步骤
- 该模式属于行为型模式
使用说明
【AbstractClass】
template方法
规定了如何调用operation2
、operation3
、operation4
这几个子方法,子方法可以是抽象方法(需要子类来实现),也可以是已经实现的方法
【ConcreteClass、ConcreteClassB】
- 用来实现父类的抽象方法
应用场景
当要完成某个过程,该过程要执行一系列步骤,这一系列的步骤基本相同,但其个别步骤可能有不同的实现,通常考虑用模板方法模式来处理
登场角色
AbstractClass(抽象类)
:该角色负责实现模板方法,还声明在模板方法中所使用到的抽象方法ConcreteClass(具体类)
:负责具体实现AbstractClass角色中定义的抽象方法。这里实现的方法将会在AbstractClass角色的模板方法中被调用Client(客户端)
:使用具体类继承的模板方法
案例实现
案例一
问题介绍
编写制作豆浆的程序,说明如下:
- 通过添加不同的配料,可以制作出不同口味的豆浆
- 制作豆浆的流程:选材—>添加配料—>浸泡—>放到豆浆机打碎,这几个步骤对于制作每种口味的豆浆都是一样的
实现
【豆浆抽象类】
package com.atguigu.template; /** * 抽象类,表示豆浆 */ public abstract class SoyaMilk { /** * 模板方法, 模板方法可以做成final , 不让子类去覆盖 */ final void make() { select(); addCondiments(); soak(); beat(); } /** * 选材料 */ void select() { System.out.println("第一步:选择好的新鲜黄豆 "); } /** * 添加不同的配料, 抽象方法, 子类具体实现 */ abstract void addCondiments(); /** * 浸泡 */ void soak() { System.out.println("第三步:黄豆和配料开始浸泡, 需要3小时 "); } /** * 打碎 */ void beat() { System.out.println("第四步:黄豆和配料放到豆浆机去打碎 "); } }
【红豆豆浆】
package com.atguigu.template; /** * 红豆豆浆 */ public class RedBeanSoyaMilk extends SoyaMilk { @Override void addCondiments() { System.out.println(" 加入上好的红豆 "); } }
【花生豆浆】
package com.atguigu.template; /** * 花生豆浆 */ public class PeanutSoyaMilk extends SoyaMilk { @Override void addCondiments() { System.out.println(" 加入上好的花生 "); } }
【主类】
package com.atguigu.template; public class Client { public static void main(String[] args) { System.out.println("----制作红豆豆浆----"); SoyaMilk redBeanSoyaMilk = new RedBeanSoyaMilk(); redBeanSoyaMilk.make(); System.out.println("----制作花生豆浆----"); SoyaMilk peanutSoyaMilk = new PeanutSoyaMilk(); peanutSoyaMilk.make(); } }
【运行】
----制作红豆豆浆---- 第一步:选择好的新鲜黄豆 加入上好的红豆 第三步:黄豆和配料开始浸泡, 需要3小时 第四步:黄豆和配料放到豆浆机去打碎 ----制作花生豆浆---- 第一步:选择好的新鲜黄豆 加入上好的花生 第三步:黄豆和配料开始浸泡, 需要3小时 第四步:黄豆和配料放到豆浆机去打碎 Process finished with exit code 0
模板方法模式的钩子方法
在模板方法模式的父类中,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为“钩子”。
应用场景:希望可以制作纯豆浆,需要添加任何配料,使用钩子方法改造上面的程序
【豆浆抽象类】
package com.atguigu.template.improve; //抽象类,表示豆浆 public abstract class SoyaMilk { /** * 模板方法, make , 模板方法可以做成final , 不让子类去覆盖. */ final void make() { select(); if(customerWantCondiments()) { // 如果需要添加配料 addCondiments(); } soak(); beat(); } /** * 选材料 */ void select() { System.out.println("第一步:选择好的新鲜黄豆 "); } /** * 添加不同的配料, 抽象方法, 子类具体实现 */ abstract void addCondiments(); /** * 浸泡 */ void soak() { System.out.println("第三步:黄豆和配料开始浸泡, 需要3小时 "); } /** * 打碎 */ void beat() { System.out.println("第四步:黄豆和配料放到豆浆机去打碎 "); } /** * 钩子方法,决定是否需要添加配料 * @return */ boolean customerWantCondiments() { return true; } }
【纯豆浆】
package com.atguigu.template.improve; /** * 纯豆浆 */ public class PureSoyaMilk extends SoyaMilk{ @Override void addCondiments() { //空实现 } /** * 如果需要自定义钩子函数,就重写这个方法,不需要就不用重写 * @return */ @Override boolean customerWantCondiments() { return false; } }
【主类】
package com.atguigu.template.improve; public class Client { public static void main(String[] args) { System.out.println("----制作纯豆浆----"); SoyaMilk pureSoyaMilk = new PureSoyaMilk(); pureSoyaMilk.make(); } }
【运行】
----制作纯豆浆---- 第一步:选择好的新鲜黄豆 第三步:黄豆和配料开始浸泡, 需要3小时 第四步:黄豆和配料放到豆浆机去打碎 Process finished with exit code 0
案例二
实现
【抽象类】
package com.atguigu.template.Sample; /** * 抽象类AbstractDisplay */ public abstract class AbstractDisplay { /** * 交给子类去实现的抽象方法(1) open */ public abstract void open(); /** * 交给子类去实现的抽象方法(2) print */ public abstract void print(); /** * 交给子类去实现的抽象方法(3) close */ public abstract void close(); /** * 本抽象类中实现的display方法,模板方法 */ public final void display() { // 首先打开… open(); // 循环调用5次print for (int i = 0; i < 5; i++) { print(); } // …最后关闭。这就是display方法所实现的功能 close(); } }
【子类:CharDisplay】
package com.atguigu.template.Sample; /** * CharDisplay是AbstractDisplay的子类 */ public class CharDisplay extends AbstractDisplay { /** * 需要显示的字符 */ private char ch; public CharDisplay(char ch) { // 保存在字段中 this.ch = ch; } public void open() { // 显示开始字符"<<" System.out.print("<<"); } public void print() { // 显示保存在字段ch中的字符 System.out.print(ch); } public void close() { // 显示结束字符">>" System.out.println(">>"); } }
【子类:StringDisplay】
package com.atguigu.template.Sample; /** * StringDisplay也是AbstractDisplay的子类 */ public class StringDisplay extends AbstractDisplay { /** * 需要显示的字符串 */ private String string; /** * 以字节为单位计算出的字符串长度 */ private int width; public StringDisplay(String string) { this.string = string; // 将字符串的字节长度也保存在字段中,以供后面使用 this.width = string.getBytes().length; } public void open() { // 调用该类的printLine方法画线 printLine(); } public void print() { // 给保存在字段中的字符串前后分别加上"|"并显示出来 System.out.println("|" + string + "|"); } public void close() { // 与open方法一样,调用printLine方法画线 printLine(); } /** * 被open和close方法调用。由于可见性是private,因此只能在本类中被调用 */ private void printLine() { // 显示表示方框的角的"+" System.out.print("+"); for (int i = 0; i < width; i++) { // 显示width个"-",和"+"一起组成边框 System.out.print("-"); } // 显示表示方框的角的"+" System.out.println("+"); } }
【主类】
package com.atguigu.template.Sample; public class Main { public static void main(String[] args) { // 生成一个持有'H'的CharDisplay类的实例 AbstractDisplay d1 = new CharDisplay('H'); // 生成一个持有"Hello, world."的StringDisplay类的实例 AbstractDisplay d2 = new StringDisplay("Hello, world."); // 生成一个持有"你好,世界。"的StringDisplay类的实例 AbstractDisplay d3 = new StringDisplay("你好,世界。"); /* * 由于d1、d2和d3都是AbstractDisplay类的子类 * 可以调用继承的display方法 * 实际的程序行为取决于CharDisplay类和StringDisplay类的具体实现 */ d1.display(); d2.display(); d3.display(); } }
【运行】
<<HHHHH>> +-------------+ |Hello, world.| |Hello, world.| |Hello, world.| |Hello, world.| |Hello, world.| +-------------+ +------------------+ |你好,世界。| |你好,世界。| |你好,世界。| |你好,世界。| |你好,世界。| +------------------+ Process finished with exit code 0
模板方法模式在IOC的源码分析
实现类有一个模板方法
另一个钩子方法
除此之外,java.io.InputStream类中也使用了Template Method
总结
【优点】
- 既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现
- 实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用。如果模板方法有bug只需要修改一个类即可,不使用模板方法模式的话,就需要修改多个类的代码
- 父类和子类具有一致性:在示例程序中,不论是CharDisplay的实例还是StringDisplay的实例,都是先保存在AbstractDisplay类型的变量中,然后再来调用display方法的。使用父类类型的变量保存子类实例的优点是,即使没有用instanceof等指定子类的种类程序也能正常工作。无论在父类类型的变量中保存哪个子类的实例,程序都可以正常工作,这种原则称为里氏替换原则
【不足】
- 不足:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大
- 一般模板方法都加上final关键字, 防止子类重写模板方法
思考
思考一
问:如果想要让示例程序中的open、print、close方法可以被具有继承关系的类和同一程序包中的类调用,但是不能被无关的其他类调用,应当怎么做呢?
答:使用protected关键字修饰这些方法,不要使用public。
【Java四种权限修饰符】
public | protected | (default) | private |
类本身 | √ | √ | √ | √ |
同一个包下的类 | √ | √ | √ | × |
不同包,但是我的子类 | √ | √ | × | × |
不同包非子类 | √ | × | × | × |
注:default不需要写出来,不写权限修饰符默认就是defaunt
思考二
问:Java中的接口与抽象类很相似。接口同样也是抽象方法的集合,但是在模板方法模式中,我们却无法使用接口来扮演AbstractClass 角色,请问这是为什么呢?
答:因为使用接口无法实现模板方法和其他的方法。
文章说明
- 本文章为本人学习尚硅谷的学习笔记,文章中大部分内容来源于尚硅谷视频(点击学习尚硅谷相关课程),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对尚硅谷的优质课程表示感谢。
- 本人还同步阅读《图解设计模式》书籍(图解设计模式/(日)结城浩著;杨文轩译–北京:人民邮电出版社,2017.1),进而综合两者的内容,让知识点更加全面