58 | 模板模式(上):剖析模板模式在JDK、Servlet、JUnit等中的应用
下载APP
关闭
渠道合作
推荐作者
58 | 模板模式(上):剖析模板模式在JDK、Servlet、JUnit等中的应用
2020-03-16 王争 来自北京
《设计模式之美》
课程介绍
讲述:冯永吉
时长08:41大小7.95M
上两节课我们学习了第一个行为型设计模式,观察者模式。针对不同的应用场景,我们讲解了不同的实现方式,有同步阻塞、异步非阻塞的实现方式,也有进程内、进程间的实现方式。除此之外,我还带你手把手实现了一个简单的 EventBus 框架。
今天,我们再学习另外一种行为型设计模式,模板模式。我们多次强调,绝大部分设计模式的原理和实现,都非常简单,难的是掌握应用场景,搞清楚能解决什么问题。模板模式也不例外。模板模式主要是用来解决复用和扩展两个问题。我们今天会结合 Java Servlet、JUnit TestCase、Java InputStream、Java AbstractList 四个例子来具体讲解这两个作用。
话不多说,让我们正式开始今天的学习吧!
模板模式的原理与实现
模板模式,全称是模板方法设计模式,英文是 Template Method Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的:
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
翻译成中文就是:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
原理很简单,代码实现就更加简单,我写了一个示例代码,如下所示。templateMethod() 函数定义为 final,是为了避免子类重写它。method1() 和 method2() 定义为 abstract,是为了强迫子类去实现。不过,这些都不是必须的,在实际的项目开发中,模板模式的代码实现比较灵活,待会儿讲到应用场景的时候,我们会有具体的体现。
模板模式作用一:复用
开篇的时候,我们讲到模板模式有两大作用:复用和扩展。我们先来看它的第一个作用:复用。
模板模式把一个算法中不变的流程抽象到父类的模板方法 templateMethod() 中,将可变的部分 method1()、method2() 留给子类 ContreteClass1 和 ContreteClass2 来实现。所有的子类都可以复用父类中模板方法定义的流程代码。我们通过两个小例子来更直观地体会一下。
1.Java InputStream
Java IO 类库中,有很多类的设计用到了模板模式,比如 InputStream、OutputStream、Reader、Writer。我们拿 InputStream 来举例说明一下。
我把 InputStream 部分相关代码贴在了下面。在代码中,read() 函数是一个模板方法,定义了读取数据的整个流程,并且暴露了一个可以由子类来定制的抽象方法。不过这个方法也被命名为了 read(),只是参数跟模板方法不同。
2.Java AbstractList
在 Java AbstractList 类中,addAll() 函数可以看作模板方法,add() 是子类需要重写的方法,尽管没有声明为 abstract 的,但函数实现直接抛出了 UnsupportedOperationException 异常。前提是,如果子类不重写是不能使用的。
模板模式作用二:扩展
模板模式的第二大作用的是扩展。这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性,有点类似我们之前讲到的控制反转,你可以结合第 19 节来一块理解。基于这个作用,模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。我们通过 Junit TestCase、Java Servlet 两个例子来解释一下。
1.Java Servlet
对于 Java Web 项目开发来说,常用的开发框架是 SpringMVC。利用它,我们只需要关注业务代码的编写,底层的原理几乎不会涉及。但是,如果我们抛开这些高级框架来开发 Web 项目,必然会用到 Servlet。实际上,使用比较底层的 Servlet 来开发 Web 项目也不难。我们只需要定义一个继承 HttpServlet 的类,并且重写其中的 doGet() 或 doPost() 方法,来分别处理 get 和 post 请求。具体的代码示例如下所示:
除此之外,我们还需要在配置文件 web.xml 中做如下配置。Tomcat、Jetty 等 Servlet 容器在启动的时候,会自动加载这个配置文件中的 URL 和 Servlet 之间的映射关系。
当我们在浏览器中输入网址(比如,http://127.0.0.1:8080/hello )的时候,Servlet 容器会接收到相应的请求,并且根据 URL 和 Servlet 之间的映射关系,找到相应的 Servlet(HelloServlet),然后执行它的 service() 方法。service() 方法定义在父类 HttpServlet 中,它会调用 doGet() 或 doPost() 方法,然后输出数据(“Hello world”)到网页。
我们现在来看,HttpServlet 的 service() 函数长什么样子。
从上面的代码中我们可以看出,HttpServlet 的 service() 方法就是一个模板方法,它实现了整个 HTTP 请求的执行流程,doGet()、doPost() 是模板中可以由子类来定制的部分。实际上,这就相当于 Servlet 框架提供了一个扩展点(doGet()、doPost() 方法),让框架用户在不用修改 Servlet 框架源码的情况下,将业务代码通过扩展点镶嵌到框架中执行。
2.JUnit TestCase
跟 Java Servlet 类似,JUnit 框架也通过模板模式提供了一些功能扩展点(setUp()、tearDown() 等),让框架用户可以在这些扩展点上扩展功能。
在使用 JUnit 测试框架来编写单元测试的时候,我们编写的测试类都要继承框架提供的 TestCase 类。在 TestCase 类中,runBare() 函数是模板方法,它定义了执行测试用例的整体流程:先执行 setUp() 做些准备工作,然后执行 runTest() 运行真正的测试代码,最后执行 tearDown() 做扫尾工作。
TestCase 类的具体代码如下所示。尽管 setUp()、tearDown() 并不是抽象函数,还提供了默认的实现,不强制子类去重新实现,但这部分也是可以在子类中定制的,所以也符合模板模式的定义。
重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
在模板模式经典的实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。
模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
课堂讨论
假设一个框架中的某个类暴露了两个模板方法,并且定义了一堆供模板方法调用的抽象方法,代码示例如下所示。在项目开发中,即便我们只用到这个类的其中一个模板方法,我们还是要在子类中把所有的抽象方法都实现一遍,这相当于无效劳动,有没有其他方式来解决这个问题呢?
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。
分享给需要的人,Ta购买本课程,你将得29元
生成海报并分享
赞 52
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
57 | 观察者模式(下):如何实现一个异步非阻塞的EventBus框架?
下一篇
59 | 模板模式(下):模板模式与Callback回调函数有何区别和联系?
精选留言(107)
- 攻城拔寨2020-03-17文末的问题,在 spring 生命周期中,InstantiationAwareBeanPostProcessorAdapter 就是解决这个问题的。 写个适配器,把所有抽象方法默认实现一下,子类继承这个 adapter 就行了。共 15 条评论218
- Eclipse2020-03-16可以借鉴AbstractList的addall实现。提供默认的方法method1...method4方法,每个方法直接抛出异常,使用模板方法的时候强制重写用到的method方法,用不到的method不用重写。共 9 条评论100
- Rayjun2020-03-16如果两个模版方法没有耦合,可以拆分成两个类,如果不能拆分,那就为每个方法提供默认实现70
- 最好的狗焕啊2020-03-17争哥,一年前就很崇拜你了,但是现在很迷茫,三年的开发经验了,一直在小公司,做的项目最多的数据量也只是十几万的用户,平常下班每天都会坚持学习两个小时,已经坚持一年半了,看了数据结构和算法,还有认真刷过题,看了网络协议,也看了框架方面的书等等,也认真做了笔记,然后想投递独角兽公司,但是简历都不通过,理由是学历和项目都没有亮点,我是本科毕业,看了网上的一些阿里或者百度这样的公司的面试题,发现自己也会,但是投递的简历都不通过,真的很迷茫,不知道这样的坚持有没有用,现在想回到老家一个二线城市,做着一份养老的工作展开共 42 条评论50
- tt2020-03-17参考装饰器模式那一课中JAVA IO类库中的做法,引入一个中间父类,实现所有的抽象方法,然后再让业务类去继承这个中间的父类。25
- 每天晒白牙2020-03-16提供一个 Base 类,实现 method1 到 method4 的所有抽象方法,然后子类继承 Base 类,一般可以直接复用 Base 类中的 method1 到 method4 方法,如果需要重写,直接重写该方法就好。这样就能省去所有子类实现所有抽象方法 继承抽象方法的基类 Base public class Base extends AbstractClass { @Override protected void method1() { System.out.println("1"); } @Override protected void method2() { System.out.println("2"); } @Override protected void method3() { System.out.println("3"); } @Override protected void method4() { System.out.println("4"); } } 子类 A 需要重写 method1 方法 public class SubA extends Base { // 只需要重写 method1 @Override public void method1() { System.out.println("重写 method1"); } public static void main(String[] args) { Base A = new SubA(); A.templateMethod1(); } } 输出结果为 重写 method1 2展开24
- 小兵2020-03-16父类中不用抽象方法,提供一个空的实现,子类根据需要重写。共 3 条评论17
- 下雨天2020-03-16课后思考: 一. 能修改框架代码情况: 定义一个父类,给不需要调用的抽象方法一个默认实现,子类继承该父类。 二. 如果可以修改框架代码的情况下: 1.templateMethod1与templateMethod2相关:可以将不需要调用的方法修改成protected并提供默认空实现。 2.templateMethod1与templateMethod2不相关:接口隔离原则,考虑将AbstractClass拆分成两个类分别定义两个方法。展开10
- 宁锟2020-03-16定义两个抽象类,继承模板类,分别给不需要的方法定义空实现共 1 条评论6
- Gopher2020-10-14不会java 所以一下没看懂模版方法模式 看了其他资料才明白 所以记录一下 我们把装修房子这件事比做模版方法,装修房子的大流程事固定不变 把安装水电,收纳柜,电视墙这些细节比做可以被子类实现的抽象方法 我们可以通过重写安装水电,收纳柜,电视墙 这些方法来自定义我们的装修风格,但是不影响整体的装修流程展开5
- 刘大明2020-03-16如果其他的类不考虑复用的话,可以将这些抽取成一个基类,就是两个抽象类。分别给不需要的方法定义空实现。3
- Geek_cead382020-11-22我觉得即便是使用装饰器还是直接重写method1-4,对于需要子类重写的方法要么抛不支持异常,要么抽象,不然子类察觉不到必须重写,导致模板函数的业务出错
作者回复: ������
共 2 条评论2 - 好饿早知道送外卖了2020-04-15感觉模板模式和抽象类的实现方式和场景相同啊? 他俩有什么区别呢?求大佬们解惑共 3 条评论2
- 付昱霖2020-03-16使用外观模式,用一个新类再次包装,只暴露需要的接口。2
- LJK2020-03-16课后作业的思考:对于必须要子类实现的方法定义为抽象方法或throw Exception,对于变动比较少但是同时也不想失去扩展性的方法添加默认实现,调用时优先获取用户自定义方法,获取不到的情况下使用默认方法2
- 李金鹏2021-07-10用组合代替继承1
- 向往的生活2020-07-03个人感觉这里的复用和扩展都是一回事呢。核心思想就是将不变和可变进行分离。共 1 条评论1
- 柏油2020-03-20可以提供一个adapter默认实现所有方法,子类重写需要的方法即可,比如Spring的InstantiationAwareBeanPostProcessorAdapter就是给所有方法提供默认实现,不过jkd1.8在接口中可以使用default默认实现,在InstantiationAwareaBeanPostProcessor接口中提供了默认实现,也就不需要在用Adapter 了1
- 自来也2020-03-16Es框架里,abstractrunable是属于包装者还是模板。感觉更像包装者。不管啥了,总之觉得这样挺好用的。父类public就好了,就能解决没必要强制重写了。共 1 条评论1
- Sinclairs2020-03-16如果项目中多次用到这个类的话, 可以单独实现一个基类来继承这个模版类, 将不需要的扩展方法进行默认实现. 项目开发中直接使用基类方法就好.1