写点什么

聊一聊模板方法模式

作者:知了一笑
  • 2023-05-15
    浙江
  • 本文字数:5028 字

    阅读完需:约 16 分钟

聊一聊模板方法模式

统一抽取,制定规范;


一、概述

模板方法模式,又叫模板模式,属于 23 种设计模式中的行为型模式。在抽象类中公开定义了执行的方法,子类可以按需重写其方法,但是要以抽象类中定义的方式调用方法。总结起来就是:定义一个操作的算法结构,而将一些步骤延迟到子类中。在不改变算法结构的情况下,子类能重定义该算法的特定步骤。


下面是模板模式的 UML 图,抽象类(AbstractClass)定义公共的步骤和方法,依次调用实际的模板方法,当然每个方法可以是抽象方法(需交给子类实现),也可以是提供默认的方法。具体的类(ConcreteClass)可以重写所有的方法,但是不能改变抽象类中定义的整体结构。


二、入门案例

相信大家都吃过蛋糕,现在市面上的蛋糕可谓是五花八门,你能想到的造型商家能给你整出来,你想不到的,他们也能整出来。不过无论造型如何变化,不变的有两种东西:“奶油”和“面包”。其余的材料随意搭配,就凑成了各式各样的蛋糕。



基于这个场景,我们来写一个案例,进一步了解下模板模式;创建三个类:Cake(蛋糕)、StrawberryCake(草莓蛋糕)、CherryCake(樱桃蛋糕)。最后创建一个Client类,实现这个制作蛋糕的调用过程。


package com.wsrf.template;
/** * @author 往事如风 * @version 1.0 * @date 2023/5/4 16:12 * @description:抽象类:蛋糕 */public abstract class Cake {
/** * 制作 */ public void make() { System.out.println("开始准备材料。"); bread(); cream(); fruit(); System.out.println("经过一系列的操作。"); System.out.println("制作完成。"); }
/** * 准备面包 */ public void bread() { System.out.println("准备材料:面包"); }
/** * 准备奶油 */ public void cream() { System.out.println("准备材料:奶油"); }
/** * 准备水果 */ protected abstract void fruit();
}
复制代码


package com.wsrf.template;
/** * @author 往事如风 * @version 1.0 * @date 2023/5/4 16:13 * @description:具体类:草莓蛋糕 */public class StrawberryCake extends Cake{ @Override protected void fruit() { System.out.println("准备材料:草莓"); }}
复制代码


package com.wsrf.template;
/** * @author 往事如风 * @version 1.0 * @date 2023/5/4 16:14 * @description:具体类:樱桃蛋糕 */public class CherryCake extends Cake{ @Override protected void fruit() { System.out.println("准备材料:樱桃"); }}
复制代码


package com.wsrf.template;
/** * @author 往事如风 * @version 1.0 * @date 2023/5/4 16:21 * @description */public class Client {
public static void main(String[] args) { Cake c1 = new CherryCake(); c1.make(); System.out.println("-------------------------------------"); Cake c2 = new StrawberryCake(); c2.make(); }}/**输出结果:开始准备材料。准备材料:面包准备材料:奶油准备材料:樱桃经过一系列的操作。制作完成。-------------------------------------开始准备材料。准备材料:面包准备材料:奶油准备材料:草莓经过一系列的操作。制作完成。*/
复制代码


Cake类中定义了制作蛋糕的整个步骤,也就是 make 方法;然后抽取了公用的方法,bread 方法和 cream 方法;最后定义一个抽象方法 fruit,这个方法需要交给具体的子类StrawberryCakeCherryCake去实现,从而定制差异化的“蛋糕”。

三、运用场景

通过上面的“蛋糕”案例,在平时开发中我们可以具体分析一下业务需求,首先在父类中定义需求需要实现的步骤,然后将可以公用的方法抽取到父类中,将个性化的方法放到具体的子类中去实现;这样可以很好的培养“抽象化”的思维模式,这是拉开差距的第一步。


最近在开发中,遇到这样的一个业务场景:需要给不同的管理人员计算各种不同的津贴,如区域总监有区域管理津贴、佣金、培养育成津贴等等。通过分析,每种不用类型的津贴,都是需要金额 x 比例 x 系数,比例每种津贴都有不同的计算方式,系数也是。所以,大致的想法就是:金额 x 比例 x 系数这个计算方式设置为统一的方法,系数和比例让具体的津贴子类去实现。所以大致的伪代码如下;


首先,我定义了一个抽象类AbstractManageAllowanceCalService,用于定义统一的计算方法,并预留了获取比例和获取系数的抽象方法。


/** * @author 往事如风 * @version 1.0 * @date 2023/5/4 17:12 * @description:津贴计算父类 */@Slf4jpublic abstract class AbstractManageAllowanceCalService {
/** * 计算津贴 * @param amount * @return */ public BigDecimal calAmount(BigDecimal amount) { if (Objects.isNull(amount)) { return BigDecimal.ZERO; } BigDecimal ratio = getRatio(); BigDecimal coefficient = getCoefficient(); log.info("金额:{},系数:{},比例:{}", amount, coefficient, ratio); return amount.multiply(ratio).multiply(coefficient); }
/** * 获取比例 * @return */ protected abstract BigDecimal getRatio();
/** * 获取系数 * @return */ protected abstract BigDecimal getCoefficient();}
复制代码


然后,定义两个具体的子类,用于计算区域管理津贴和佣金。


/** * @author 往事如风 * @version 1.0 * @date 2023/5/4 17:17 * @description:区域管理津贴计算 */@Servicepublic class AreaBusinessAllowanceCalService extends AbstractManageAllowanceCalService{    /**     * 区域管理津贴比例     * @return     */    @Override    protected BigDecimal getRatio() {        return new BigDecimal(0.5).setScale(1, BigDecimal.ROUND_HALF_UP);    }
/** * 区域管理津贴系数 * @return */ @Override protected BigDecimal getCoefficient() { return new BigDecimal(0.92).setScale(2, BigDecimal.ROUND_HALF_UP); }}
复制代码


/** * @author 往事如风 * @version 1.0 * @date 2023/5/4 17:19 * @description:佣金计算 */@Servicepublic class SalaryCalService extends AbstractManageAllowanceCalService{    /**     * 佣金比例     * @return     */    @Override    protected BigDecimal getRatio() {        return new BigDecimal(0.45).setScale(2, BigDecimal.ROUND_HALF_UP);    }
/** * 佣金系数 * @return */ @Override protected BigDecimal getCoefficient() { return new BigDecimal(0.88).setScale(2, BigDecimal.ROUND_HALF_UP); }}
复制代码


最后,定义一个 controller 类,用于接口调用,提供计算能力;接收两个参数,金额和计算津贴类型。


/** * @author 往事如风 * @version 1.0 * @date 2023/5/4 17:21 * @description */@RestController@RequestMapping("/cal")public class CalController implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@PostMapping("/amount") public Result<BigDecimal> calAmount(BigDecimal amount, String calType) { AbstractManageAllowanceCalService service = null; if ("AREA".equals(calType)) { // 区域管理津贴 service = (AbstractManageAllowanceCalService) applicationContext.getBean("areaBusinessAllowanceCalService"); } else if ("SALARY".equals(calType)) { // 佣金 service = (AbstractManageAllowanceCalService) applicationContext.getBean("salaryCalService"); } if (Objects.nonNull(service)) { return Result.success(service.calAmount(amount)); } return Result.fail(); }
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { CalController.applicationContext = applicationContext; }}
复制代码


在这个 controller 类中,我通过分析“类型”这个参数,来判断需要调用哪个 service 去实现具体的计算逻辑。这里用了 if-else 的方式去实现;其实也可以用到另一个设计模式——策略模式,这样写出来的代码就会比较优雅,这里就不对策略模式展开赘述了。

四、源码中运用

4.1、JDK 源码中的模板模式

在 JDK 中其实也有很多地方运用到了模板模式,这里咱挑一个讲。并发包下的AbstractQueuedSynchronizer类,就是一个抽象类,也就是我们先前的文章中提到过的 AQS。


public abstract class AbstractQueuedSynchronizer    extends AbstractOwnableSynchronizer    implements java.io.Serializable {      public final void acquire(int arg) {        if (!tryAcquire(arg) &&            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))            selfInterrupt();    }
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }}
复制代码


其中,tryAcquire 和 tryRelease 这两个方式直接抛了异常,用 protected 关键词修饰,需要由子类去实现。然后再 acquire 和 release 方法中分别去调用这两方法。也就是 acquire 方法定义了一个统一的结构,差异化的 tryAcquire 方法需要具体的子类去实现功能,实现了模板模式。

4.2、Spring 源码中的模板模式

说到源码,Spring 是一个绕不开的话题,那就来学习下 Spring 中的模板模式。其中,有一个类DefaultBeanDefinitionDocumentReader,它是BeanDefinitionDocumentReader的实现类,是提取 spring 配置文件中的 bean 信息,并转化为 BeanDefinition。


public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader {      protected void doRegisterBeanDefinitions(Element root) {        BeanDefinitionParserDelegate parent = this.delegate;        this.delegate = this.createDelegate(this.getReaderContext(), root, parent);        //...
this.preProcessXml(root); this.parseBeanDefinitions(root, this.delegate); this.postProcessXml(root); this.delegate = parent; }
protected void preProcessXml(Element root) { }
protected void postProcessXml(Element root) { }}
复制代码


这里我截图了其中的一段代码,主要是 doRegisterBeanDefinitions 这个方法,从跟节点 root 出发,root 下的每个 bean 注册定义。


该方法中还调用了 preProcessXml 和 postProcessXml 这两个方法,但是在DefaultBeanDefinitionDocumentReader类中,这两个方法是未实现的,需要其子类去实现具体的逻辑。所以,这里也是一个很典型的模板模式的运用。

五、总结

模板方法模式其实是一个比较简单的设计模式,它有如下优点:1、封装不变的逻辑,扩展差异化的逻辑;2、抽取公共代码,提高代码的复用性;3、父类控制行为,子类实现细节。


其缺点就是不同的实现都需要一个子类去维护,会导致子类的个数不断增加,造成系统更加庞大。


用一句话总结:将公用的方法抽取到父类,在父类中预留可变的方法,最后子类去实现可变的方法。


模板模式更多的是考察我们对于公用方法的提取;对于编程也是这样,更多的是一种思维能力,不能只局限于代码,要把格局打开。

六、参考源码

编程文档:https://gitee.com/cicadasmile/butte-java-note
应用仓库:https://gitee.com/cicadasmile/butte-flyer-parent
复制代码


用户头像

知了一笑

关注

公众号:知了一笑 2020-04-08 加入

源码仓库:https://gitee.com/cicadasmile

评论

发布
暂无评论
聊一聊模板方法模式_设计模式_知了一笑_InfoQ写作社区