模板方法模式
定义
定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
通用类图
意图
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
优点
- 封装不变部分,扩展可变部分。
- 提取公共代码,便于维护。
- 行为由父类控制,子类实现。
缺点
按照我们的设计习惯,抽象类负责声明最抽象、最一般的事物属性和方法,实现类完成具体的事物属性和方法。但是模板方法模式却颠倒了,抽象类定义了部分抽象方法,由子类实现,子类执行的结果影响了父类的结果,也就是子类对父类产生了影响,这在复杂的项目中,会带来代码阅读的难度,而且也会让新手产生不适感。
应用场景
- 多个子类有公有的方法,并且逻辑基本相同时。
- 重要复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。
- 重构时,模板方法模式是一个经常使用的模式,把相同的代码 抽取到父类中。
扩展
钩子函数,通过钩子函数可以让子类决定父类的行为。
说明
模板方法其实主要是定义一个通用的算法框架(这里的算法说得是为解决事情而需要执行的一系列步骤),将通用的抽象的部分定义在父类中,变化的具体的部分只是在父类中声明抽象的方法,由子类去实现具体的内容。
话不多说上代码,这里只为了演示,并发线程安全的问题暂不做考虑。
在由事件驱动的系统中,肯定会用到消息队列,这里模拟一个利用消息队列处理平时系统产生的各种消息(用户注册,下订单,上传产品,扣减库存等)的场景。
假设有一个所有消息的基本的父类
class BaseMsg { Date date; public Date getDate() { return date; } public void setDate(Date date) { this.date = date; }}
消息A
class MsgA extends BaseMsg { String content; public String getContent() { return content; } public void setContent(String content) { this.content = content; }}
消息B
class MsgB extends BaseMsg{ String content; public String getContent() { return content; } public void setContent(String content) { this.content = content; }}
用 ConcurrentLinkedQueue 模拟消息队列,现实中可能用到的是 ActiveMQ, RabbitMQ 等
public class CacheBuffer { public static final Queue CACHE_BUFFER = new ConcurrentLinkedQueue();}
下面就是我们的模版类了
public abstract class AbstractMsgHandler{ private boolean isDo = true; protected void put(E e) { CACHE_BUFFER.offer(e); } protected void process() { E e = (E) CACHE_BUFFER.poll(); if (checkValid(e)) { setProcessTime(e); if (isDo()) { doAnotherThing(); } doLogic(e); } } private boolean checkValid(E e) { System.out.println("检查字段是否为空,对象是否合法等等"); return true; } private void setProcessTime(E e) { System.out.println("设置处理时间等于当前时间,记录到日志"); } private void doAnotherThing() { System.out.println("做其他的特别的事情"); } protected abstract void doLogic(E e); protected boolean isDo () { return isDo; } public void setDo(boolean aDo) { isDo = aDo; }}
在模版类中添加了一些通用的方法
put 方法的是添加消息到队列
process 方法的是从消息队列中取出消息,并且检查了有效性(checkValid),设置了处理时间(setProcessTime),还有其他事情(doAnotherThing),接下来是处理业务逻辑(doLogic)接下来是 消息A的处理类
public class ConcreteMsgAHandler extends AbstractMsgHandler{ @Override protected void doLogic(MsgA msgA) { System.out.println("MsgA 处理业务逻辑"); }}
消息B的处理类
public class ConcreteMsgBHandler extends AbstractMsgHandler{ @Override protected void doLogic(MsgB msgB) { System.out.println("MsgB 处理业务逻辑"); }}
各自实现了 doLogic 方法,实现自己的业务逻辑。
场景类
public static void main(String[] args) { MsgA msgA = new MsgA(); msgA.setContent("a"); MsgB msgB = new MsgB(); msgB.setContent("b"); ConcreteMsgAHandler msgAHandler = new ConcreteMsgAHandler(); ConcreteMsgBHandler msgBHandler = new ConcreteMsgBHandler(); msgAHandler.put(msgA); msgBHandler.put(msgB); msgAHandler.process(); msgBHandler.process();}
运行结果为
检查字段是否为空,对象是否合法等等设置处理时间等于当前时间,记录到日志做其他的特别的事情MsgA 处理业务逻辑检查字段是否为空,对象是否合法等等设置处理时间等于当前时间,记录到日志做其他的特别的事情MsgB 处理业务逻辑
实现了模版方法的子类(ConcreteMsgAHandler, ConcreteMsgBHandler)在执行了 process 方法之后都会检查字段是否为空,设置处理时间,做其他事情,然后做自己的业务。
process 方法为模板方法,里面的代码块即我们这里的算法框架,定义了通用的算法,或者说封装了不变的部分。doLogic 方法由子类实现具体的逻辑。这样如果还有一个消息C(MsgC),我们只要创建一个 ConcreteMsgCHandler 并且实现 doLogic 方法即可。
扩展和维护起来非常方便。修改通用算法只需修改模板方法,扩展只需新建子类。
在模板方法中还有一个 setDo 方法,这个方法即是模板的方法的钩子函数,如果某个子类不需要通用算法的某个部分,可以通过 setDo 来改变父类通用算法的行为。
public static void main(String[] args) { MsgA msgA = new MsgA(); msgA.setContent("a"); MsgB msgB = new MsgB(); msgB.setContent("b"); ConcreteMsgAHandler msgAHandler = new ConcreteMsgAHandler(); ConcreteMsgBHandler msgBHandler = new ConcreteMsgBHandler(); msgAHandler.put(msgA); msgBHandler.put(msgB); msgAHandler.get(); msgBHandler.setDo(false); msgBHandler.get();}
输出的结果为
检查字段是否为空,对象是否合法等等设置处理时间等于当前时间,记录到日志做其他的特别的事情MsgA 处理业务逻辑检查字段是否为空,对象是否合法等等设置处理时间等于当前时间,记录到日志MsgB 处理业务逻辑
对比上面的输出就可以发现消息B不再 做其他的特别的事情 了