聊一聊模板方法模式

news2024/11/24 1:06:50

统一抽取,制定规范;

一、概述

模板方法模式,又叫模板模式,属于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:津贴计算父类
 */
@Slf4j
public 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:区域管理津贴计算
 */
@Service
public 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:佣金计算
 */
@Service
public 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、父类控制行为,子类实现细节。

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/526896.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c语言实现栈(顺序栈,链栈)

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏: &#x1f354;&#x1f35f;&#x1f32f;C语言进阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f349;本篇简介:>:讲解用c语言实现:“数据结构之"栈”,分别从"顺序栈…

区间预测 | MATLAB实现QRCNN-BiGRU卷积双向门控循环单元分位数回归时间序列区间预测

区间预测 | MATLAB实现QRCNN-BiGRU卷积双向门控循环单元分位数回归时间序列区间预测 目录 区间预测 | MATLAB实现QRCNN-BiGRU卷积双向门控循环单元分位数回归时间序列区间预测效果一览基本介绍模型描述程序设计参考资料 效果一览 基本介绍 1.Matlab实现基于QRCNN-BiGRU分位数回…

MySQL视图与联集

一、VIEW&#xff08;视图&#xff09; 1、 概念 可以被当作是虚拟表或存储查询 视图跟表格的不同是&#xff0c;表格中有实际储存资料&#xff0c;而视图是建立在表格之上的一个架构&#xff0c;它本身并不实际储存资料。 临时表在用户退出或同数据库的连接断开后就自动消…

DIY技巧:微星B760主板13600K降压教程 CPU温度暴降25℃

前段时间微星B600/700系主板更新了最新的BIOS&#xff0c;最新的BIOS更新&#xff1b;额105微码&#xff0c;让用户能直接在BIOS中对13代带K处理器进行降压&#xff0c;十分方便&#xff0c;今就带大家体验一下微星B760迫击炮主板的降压流程&#xff0c;其他微星B600/700系主板…

43岁,年薪200万的高管,被裁了!这4条职场潜规则,你越早知道越好

作者| Mr.K 编辑| Emma 来源| 技术领导力(ID&#xff1a;jishulingdaoli) 我的一位老朋友S总&#xff0c;是某世界500强外企中国区运营总监&#xff0c;光年薪就200万&#xff0c;还不包括福利、股票的部分&#xff0c;他比我略长一两岁&#xff0c;我们人生经历相似&#xf…

一文搞懂Go错误链

0. Go错误处理简要回顾 Go是一种非常强调错误处理的编程语言。在Go中&#xff0c;错误被表示为实现了error接口的类型的值&#xff0c;error接口只有一个方法&#xff1a; type error interface {Error() string } 这个接口的引入使得Go程序可以以一致和符合惯用法的方式进行错…

Python实现哈里斯鹰优化算法(HHO)优化LightGBM回归模型(LGBMRegressor算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 2019年Heidari等人提出哈里斯鹰优化算法(Harris Hawk Optimization, HHO)&#xff0c;该算法有较强的全…

有主题的图文内容创作 | AIGC实践

话说&#xff0c;昨天我发布了第一篇&#xff0c;内容由ChatGPT和Midjourney协助完成的文章&#xff1a;胡同与侏罗纪公园的时空交错 | 胡同幻想 在这篇文章中&#xff0c;大约70%图文内容由ChatGPT和Midjourney输出。我个人参与的部分&#xff0c;主要是提出指令&#xff08;P…

Monaco Editor编辑器教程(三一):在编辑器中实现模拟调试的交互

前言 最近有小伙伴咨询如何在编辑中实现 像vscode调试代码那样,可以打断点,可以高亮当前运行的一行。这样的需求并不多见,如果要做,那肯定是对编辑器做一个深层次的定制。一般很少很少会实现这种在浏览器中调试。 目前我还没见过,如果有遇到过的朋友可以指点一下。我去学…

Cesium AI GPT 文档 源码 ChatGPT问答

我用Cesium104.0的 源码 | 文档 | 3DTiles标准 作为上下文语料定制了一个智能Cesium专家问答助手 语料: 3D Tiles Specificationhttps://cesium.com/downloads/cesiumjs/releases/1.104/Build/CesiumUnminified/Cesium.jshttps://github1s.com/CesiumGS/cesium/blob/HEAD/Doc…

redis中的管道

Redis 管道 文章目录 1. 前言2. Redis 管道3. 小总结 1. 前言 通过一个问题引出 我们接下来要学习的 Redis 管道 : 提问 : 如何优化频繁命令往返造成的性能瓶颈 ? 另外 &#xff1a; 关于上面这个问题的由来 也可以简单的说一说 上面所说的思路 其实就是管道的概念 &#xff0…

读俞敏洪的书

没有认真写过一篇关于书籍的读后感文章&#xff0c;但在读完俞敏洪老师这本书后&#xff0c;想推荐给大家&#xff0c;也想分享下我的想法。 几周前&#xff0c;我在微信读书首页看到了俞敏洪老师的读书推荐 《在绝望中寻找希望》——俞敏洪写给迷茫不安的年轻人 有好几个晚上&…

电池只能充电500次?别太荒谬!收下这份真正的充电秘籍

我们的生活已经离不开电子设备了&#xff0c;而电子设备嘛&#xff0c;又离不开给它们提供能源的电池。在网上有许许多多的“延长电池寿命小技巧”&#xff0c;比如“新买的电子设备&#xff0c;第一次充电之前要把电都放完”“笔记本电脑一直插着电源可以保护电池”“长期不用…

区分COCO数据集的coco minival和coco test-dev、conda常用命令和python -m 的作用

1、COCO数据集的测试集coco minival和coco test-dev: 两个数据集在官方网站对应的内容如下所示&#xff1a; COCO数据集官网&#xff1a;https://cocodataset.org/#download 两个数据集的区分参考网址&#xff1a;https://zhuanlan.zhihu.com/p/533676547 2、conda常用命令…

linux中epoll+socket实战

目录 参考前言案例 一、epoll的基本使用首先是epoll_create函数&#xff1a;然后是epoll_ctl函数&#xff1a;最后是epoll_wait函数&#xff1a;关于ET&#xff08;边沿触发&#xff09;、LT&#xff08;水平触发&#xff09;两种工作模式可以得出这样的结论: 二、使用代码简易…

基于html+css的图展示71

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

某音X-Bogus算法研究 2023-05-15

本文以教学为基准&#xff0c;研究JavaScript算法及反爬策略、本文提供的可操作性不得用于任何商业用途和违法违规场景。 如有侵权&#xff0c;请联系我进行删除。 今天我们分析一下douyin个人主页数据获取。 大多数小伙伴应该都知道想要拿到douyin的数据也不是那么容易的&a…

近世代数 笔记与题型连载 第十三章(环与域)

文章目录 基本概念1.环1.1.环的定义1.2.环的性质1.3.几种特殊的环1.4.子环 2.域2.1.域的定义2.2.环与域的同态 相关题型1.验证一个代数系统是否是一个环2.判断一个代数系统是否是整环3.判断一个代数系统是否是另一个代数系统的子环4.判断一个代数系统是否是域 基本概念 1.环 …

sed编辑器命令

sed编辑器 sed是一种流编辑器&#xff0c;流编辑器会在编辑器处理数据之前基于预先提供的一组规则来编辑数据流。 sed编辑器可以根据命令来处理数据流中的数据&#xff0c;这些命令要么从命令行中输入&#xff0c;要么存储在一个命令文本文件中。 sed 的工作流程主要包括读取…

Nat. Commun | 中国海洋大学张伟鹏组揭示海洋生物被膜群落硫氧化主要菌群及其作用机制...

海洋生物被膜玫瑰杆菌的厌氧硫氧化机制 Anaerobic thiosulfate oxidation by the Roseobacter group is prevalent in marine biofilms Article&#xff0c;2022-04-11&#xff0c;Nature Communications&#xff0c;[IF 17.7] DOI&#xff1a;10.1038/s41467-023-37759-4 原文…