模板方法模式:命题作文
模板,原义是指带有镂空文字的薄薄塑料板。只要用笔在模板的镂空处进行临摹,即使是手写也能写出整齐的文字。虽然只要看到这些镂空的洞,我们就可以知道能写出哪些文字,但是具体写出的文字是什么感觉则依赖于所用的笔。如果使用签字笔来临摹,则可以写出签字似的文字;如果使用铅笔来临摹,则可以写出铅笔字;而如果是用彩色笔临摹,则可以写出彩色的字。但是无论使用什么笔,文字的形状都会与模板上镂空处的形状一致。
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.——GoF
在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
这里的「算法」可理解为广义上的「业务逻辑」。模板方法模式在父类中定义处理流程的框架,允许子类中实现每个流程的具体处理。
登场角色:
AbstractClass
(抽象类):实现模板方法,声明在模板方法中所使用到的抽象方法。这些方法由子类ConcreteClass
角色负责实现。ConcreteClass
(具体类):实现AbstractClass
角色中定义的抽象方法。这里的实现方法会在 AbstractClass 角色得模板方法中被调用。
在模板模式经典的实现中,模板方法定义为
final
,可以避免被子类重写。需要子类重写的方法定义为abstract
,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。
观察上面的 AbstractClass
,其中 templateMethod
为模板方法,里面使用了 method1
、method2
、method3
抽象方法。这些抽象方法需要子类去实现。
代码示例:
1 | public class TemplateTest { |
类的层次与抽象类:
- 父类对子类的要求(子类责任):父类期待子类去实现抽象方法;父类要求子类去实现抽象方法。
- 抽象类的意义:在抽象类阶段确定处理的流程。
- 父类子类协作:降低子类灵活性,子类可能会臃肿。
拓展思路要点:
- 父类的模板方法中编写了算法,因此无需在每个子类中再编写算法。模板方法出现 Bug 时,只需改模板方法。
- 父类子类紧密联系。在子类中实现父类中声明的抽象方法时,必须要理解抽象方法被调用的时机。
- 父类与子类的一致性,里氏替换原则 LSP。使用父类类型的变量保存子类实例的优点是,即使没有用
instanceof
等指定子类的种类,程序也能正常工作。LSP 是通用的继承原则,并非仅限于模板方法模式。
两大作用:
- 复用:所有的子类都可以复用父类中模板方法定义的流程代码。
- 扩展:这里所说的扩展,并不是指代码的扩展性,而是指框架的扩展性。模板模式常用在框架的开发中,让框架用户可以在不修改框架源码的情况下,定制化框架的功能。比如,在模板方法中,会调用一些抽象方法,而这些方法的具体实现可以交由子类去扩展。
应用场景:
- 算法的整体框架已经确定,但某些具体步骤的实现可能会有所不同。模板方法模式可以在抽象类中定义整体的算法框架,而将具体步骤的实现留给子类来完成。
- 多个类具有相似的算法结构,但其中的某些步骤可能有所不同。通过使用模板方法模式,可以将这些公共的代码逻辑提取到抽象类中的模板方法中,避免代码的重复。
- 需要在不破坏原有算法框架的情况下,对算法中的某些步骤进行扩展或修改。通过使用模板方法模式,可以在抽象类中定义通用的算法流程,而在子类中重写需要修改或扩展的具体步骤。
- 需要在多个相关类中实现一些公共行为,而不希望将这些行为放在一个单独的类中管理。模板方法模式可以将这些公共行为封装在抽象类中的模板方法中,使得不同的类可以根据需要继承该抽象类并实现具体的步骤。
- 需要控制算法的执行顺序和流程。通过模板方法模式,可以在抽象类中控制算法的整体结构,确保每个步骤按照预期顺序执行。
相关的设计模式
站内文章工厂(方法)模式 是将模板方法模式用于生成实例的一个典型例子。
与策略模式的对比:
设计模式 | 模板方法模式 | 策略模式 |
---|---|---|
改变程序行为的技术 | 继承。在父类中定义程序行为的框架,在子类中决定具体的处理。 | 委托。 |
关注点 | 算法的整体框架、步骤。 | 不同的算法实现。将算法的实现与具体的类解耦。 |
改变程序行为 | 子类可以改变部分程序行为。 | 替换整个算法。 |
Java 源代码中的模板方法模式
InputStream
、OutputStream
、Reader
、Writer
类都使用了模板方法模式。AbstractList
的 addAll()
函数也使用了模板方法模式。
Java 中的 Servlet 生命周期就是使用了模板方法模式,其中抽象类 HttpServlet
定义了 doGet
、doPost
等操作方法,而具体的 Servlet
类可以继承 HttpServlet
并重写具体的操作方法来实现自定义的业务逻辑。
回调函数
相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是「回调函数」。A 调用 B,B 反过来又调用 A,这种调用机制就叫作「回调」。
对于回调,不同的编程语言有不同的实现方式,C 语言中使用函数指针来实现。
回调可以分为同步回调和异步回调(或者延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指的是在函数返回之后执行回调函数。
同步回调
Java 中同步回调函数的典型实现:
1 | public interface ICallback { |
同步回调更像是模板方法模式:将不变的执行流程抽离出来,放到模板方法中,然后将可变的部分实际成回调,由用户来定制。
异步回调
比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的 URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。
回调与模板模式
从应用场景上来看,同步回调看起来更像模板模式,它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。异步回调看起来更像观察者模式。
从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。
组合优于继承。在代码实现上,回调相对于模板模式会更加灵活,主要体现在:
- 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
- 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
- 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。
回调与钩子
Callback 是一种异步调用的实现,Callback 本意就是你传递一个函数给对方,当对方的工作有进展的时候就调用这个函数通知你。
Hook 则是一种 API 拦截手段,特点就是不改变原有双边的逻辑的情况下,在 API 接口上插入一个拦截调用的 Hook 函数,从而截取调用数据、甚至可以改变程序行为。
Java 8 之后的模板方法模式新写法
Java 8 中最大的新特性就是提供了对 站内文章函数式编程 的支持。Java 8 在 java.util.function
下面增加增加一系列的函数接口。其中主要有 Consumer
、Supplier
、Predicate
、Function
等。
在上文中,我们可以知道模板方法模式中,子类通过实现抽象方法替换父类算法框架中的某个步骤。但是如果父类的模板方法中,一些步骤可以进行多种替换。如果仅靠子类实现将会需要编写许多的类。
我们可以进行以下改进:
- 使用函数式编程,让算法步骤作为一个函数类型传入到模板方法中
- 在统一的类中定义不同的算法实现,免去继承的麻烦
文章开头的案例可以改造为:
1 | public class TemplateTest { |
长得很像上一节讲的同步回调。
本文 PlantUML 归档
1 | abstract class AbstractClass{ |
本文参考
- 《图解设计模式》
- 本科生课程笔记《程序设计中级实践&设计模式》 - TJU 🍐⚱️
- 极客时间专栏 - 设计模式之美 - 王争
- 钩子函数(HOOK)和回调函数(CALLBACK)有什么区别? - 知乎
- 使用Java8改造出来的模板方法真的是yyds-阿里云开发者社区
- JAVA设计模式13:模版方法模式,将一些步骤延迟到子类中实现-腾讯云开发者社区-腾讯云